mirror of
https://github.com/SK8-ENGINE/skate-3-rust-engine.git
synced 2026-10-26 19:47:44 +00:00
Implement stock bump reaction blend space and graph behavior
This commit is contained in:
1 parent
792056ca8e
commit
b4f5b67d5d
22 files changed
+1049
-2
No files matched your search
@@ -0,0 +1,69 @@
|
||||
//! SetBumpCoefficients Begin82BB0660, ctor82BC7318/vtable823200C8+48.
|
||||
use crate::physics::native_arithmetic;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Settings {
|
||||
pub scale_x_acc: f32,
|
||||
pub min_bump_mag: f32,
|
||||
pub min_bump_blend_value: f32,
|
||||
pub max_bump_mag: f32,
|
||||
}
|
||||
///Input is PhysOutAnimation112 (already ground-conditioned), not raw world acceleration.
|
||||
pub fn coefficients(mut acceleration: [f32; 4], mirrored: bool, settings: &Settings) -> [f32; 2] {
|
||||
acceleration[0] *= settings.scale_x_acc;
|
||||
let squared = native_arithmetic::dot3(acceleration, acceleration);
|
||||
let mut inverse = native_arithmetic::reciprocal_square_root_estimate(squared);
|
||||
for _ in 0..2 {
|
||||
inverse = (inverse * 0.5).mul_add((-squared).mul_add(inverse * inverse, 1.), inverse);
|
||||
}
|
||||
let magnitude = if squared == 0. { 0. } else { squared * inverse };
|
||||
let t = ((magnitude - settings.min_bump_mag) / (settings.max_bump_mag - settings.min_bump_mag))
|
||||
.clamp(0., 1.);
|
||||
let weight = t.mul_add(
|
||||
1. - settings.min_bump_blend_value,
|
||||
settings.min_bump_blend_value,
|
||||
);
|
||||
//Initializer82F826F8 broadcasts82181A88 into830BD350: length threshold1e-6.
|
||||
let direction = if magnitude > f32::from_bits(0x3586_37bd) {
|
||||
[acceleration[0] * inverse, acceleration[2] * inverse]
|
||||
} else {
|
||||
[0.; 2]
|
||||
};
|
||||
direction.map(|v| {
|
||||
let value = v * weight;
|
||||
if mirrored { -value } else { value }
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn bump_direction_scaling_thresholds_and_stance() {
|
||||
let s = Settings {
|
||||
scale_x_acc: 2.,
|
||||
min_bump_mag: 10.,
|
||||
min_bump_blend_value: 0.2,
|
||||
max_bump_mag: 30.,
|
||||
};
|
||||
assert_eq!(coefficients([0.; 4], false, &s), [0.; 2]);
|
||||
for (input, expected) in [
|
||||
([5., 0., 0., 99.], [0.2, 0.]),
|
||||
([10., 0., 0., 0.], [0.6, 0.]),
|
||||
([0., 0., -40., 0.], [0., -1.]),
|
||||
([0.0000001, 0., 0., 0.], [0., 0.]),
|
||||
] {
|
||||
let result = coefficients(input, false, &s);
|
||||
let mirrored = coefficients(input, true, &s);
|
||||
for i in 0..2 {
|
||||
assert!(
|
||||
(result[i] - expected[i]).abs() < 1e-6,
|
||||
"{result:?} != {expected:?}"
|
||||
);
|
||||
assert_eq!(mirrored[i], -result[i]);
|
||||
}
|
||||
}
|
||||
let diagonal = coefficients([12., 0., 18., 0.], false, &s);
|
||||
assert!((diagonal[0] - 0.8).abs() < 1e-6 && (diagonal[1] - 0.6).abs() < 1e-6);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
//! Animation timing and event semantics recovered from Skate 3 TU3.
|
||||
pub mod bump;
|
||||
pub mod clip_clock;
|
||||
pub mod commands;
|
||||
pub mod output;
|
||||
|
||||
@@ -47,6 +47,37 @@ pub fn blend(left: &mut AnimationAttribute, right: &AnimationAttribute, weight:
|
||||
Ok(())
|
||||
}
|
||||
|
||||
///82D16370. Keep reference names and event flags; scale only numeric payload.
|
||||
pub fn scale(attribute: &mut AnimationAttribute, weight: f32) -> Result<(), String> {
|
||||
if attribute.status & 2 != 0 {
|
||||
attribute.begin_time = -1.; attribute.end_time = -1.;
|
||||
} else {
|
||||
attribute.begin_time *= weight; attribute.end_time *= weight;
|
||||
}
|
||||
for &i in numeric_lanes(attribute.kind) {
|
||||
let value = f32::from_bits(attribute.payload.0[i].ok_or("Uninitialized weighted attribute")?);
|
||||
attribute.payload.0[i] = Some((value*weight).to_bits());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
///82D16428. Time adds are separate instructions; numeric payload uses FMA.
|
||||
pub fn add_weighted(left: &mut AnimationAttribute, right: &AnimationAttribute, weight: f32) -> Result<(), String> {
|
||||
if left.status & 2 == 0 {
|
||||
left.begin_time += right.begin_time*weight;
|
||||
left.end_time += right.end_time*weight;
|
||||
}
|
||||
for &i in numeric_lanes(left.kind) {
|
||||
let a = f32::from_bits(left.payload.0[i].ok_or("Uninitialized weighted attribute")?);
|
||||
let b = f32::from_bits(right.payload.0[i].ok_or("Uninitialized weighted attribute")?);
|
||||
let value = b.mul_add(weight,a);
|
||||
left.payload.0[i] = Some(value.to_bits());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn numeric_lanes(kind:u8) -> &'static [usize] {
|
||||
match kind {0|2 => &[0],1 => &[0,1,2,3],3 => &[5],_ => &[]}
|
||||
}
|
||||
|
||||
/// Native uses a forward-only intersection, retaining left order/metadata.
|
||||
/// It does not append right-only attributes or replace the left reference name.
|
||||
pub fn intersection(left: Vec<AnimationAttribute>, right: &[AnimationAttribute], weight: f32) -> Result<Vec<AnimationAttribute>, String> {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! Owned animation tree evaluation; stock data constructs this topology.
|
||||
pub mod selection_space;
|
||||
pub mod blend_space;
|
||||
use super::{
|
||||
clip_clock::AdvanceResult,
|
||||
output::attributes::{AnimationAttribute, AttributeName},
|
||||
@@ -15,6 +16,7 @@ pub enum PlaybackTree {
|
||||
clip: PlaybackClip,
|
||||
},
|
||||
PhaseBlend(PhaseBlend),
|
||||
BlendSpace(blend_space::BlendSpace),
|
||||
SelectionSpace(selection_space::SelectionSpace),
|
||||
Transition(super::playback_transition::PlaybackTransition),
|
||||
///SkaterAnim::AddBindPose82B98118, latched when a named tree is created.
|
||||
@@ -39,6 +41,7 @@ pub enum PoseCommand {
|
||||
Blend {
|
||||
weight: f32,
|
||||
},
|
||||
WeightedBlend { weights: Vec<f32> },
|
||||
ChannelBlend {
|
||||
weight: f32,
|
||||
use_channels_from_weights: bool,
|
||||
@@ -63,6 +66,7 @@ impl PlaybackTree {
|
||||
match self {
|
||||
Self::Clip { clip, .. } => clip.clock.length,
|
||||
Self::PhaseBlend(tree) => tree.length,
|
||||
Self::BlendSpace(tree) => tree.length(),
|
||||
Self::SelectionSpace(tree) => tree.current().map_or(0.0, Self::length),
|
||||
Self::Transition(tree) => tree.to.length(),
|
||||
Self::BindPose { motion, .. } => motion.length(),
|
||||
@@ -72,6 +76,7 @@ impl PlaybackTree {
|
||||
match self {
|
||||
Self::Clip { clip, .. } => clip.clock.sample_time(),
|
||||
Self::PhaseBlend(tree) => tree.time,
|
||||
Self::BlendSpace(tree) => tree.time,
|
||||
Self::SelectionSpace(tree) => tree.current().map_or(0.0, Self::time),
|
||||
Self::Transition(tree) => tree.to.time(),
|
||||
Self::BindPose { motion, .. } => motion.time(),
|
||||
@@ -81,6 +86,7 @@ impl PlaybackTree {
|
||||
match self {
|
||||
Self::Clip { clip, .. } => clip.clock.set_time(time),
|
||||
Self::PhaseBlend(tree) => tree.set_time(time),
|
||||
Self::BlendSpace(tree) => tree.set_time(time),
|
||||
Self::SelectionSpace(tree) => tree.set_time(time),
|
||||
Self::Transition(tree) => tree.to.set_time(time),
|
||||
Self::BindPose { motion, .. } => motion.set_time(time),
|
||||
@@ -90,6 +96,7 @@ impl PlaybackTree {
|
||||
match self {
|
||||
Self::Clip { clip, .. } => clip.clock.set_speed(speed),
|
||||
Self::PhaseBlend(tree) => tree.set_speed(speed),
|
||||
Self::BlendSpace(tree) => tree.set_speed(speed),
|
||||
Self::SelectionSpace(tree) => tree.set_speed(speed),
|
||||
Self::Transition(tree) => tree.to.set_speed(speed),
|
||||
Self::BindPose { motion, .. } => motion.set_speed(speed),
|
||||
@@ -99,6 +106,7 @@ impl PlaybackTree {
|
||||
match self {
|
||||
Self::Clip { clip, .. } => clip.clock.advance(dt, phase, property),
|
||||
Self::PhaseBlend(tree) => tree.advance(dt, phase, property),
|
||||
Self::BlendSpace(tree) => tree.advance(dt, phase, property),
|
||||
Self::SelectionSpace(tree) => {
|
||||
if let Some(child) = tree.current_mut() {
|
||||
child.advance(dt, phase, property);
|
||||
@@ -112,6 +120,7 @@ impl PlaybackTree {
|
||||
match self {
|
||||
Self::Clip { .. } => Ok(false),
|
||||
Self::PhaseBlend(tree) => tree.set_attributes(attributes),
|
||||
Self::BlendSpace(tree) => tree.set_attributes(attributes),
|
||||
Self::SelectionSpace(tree) => tree.set_attributes(attributes),
|
||||
Self::Transition(tree) => tree.set_attributes(attributes),
|
||||
Self::BindPose { motion, .. } => motion.set_attributes(attributes),
|
||||
@@ -121,6 +130,7 @@ impl PlaybackTree {
|
||||
match self {
|
||||
Self::Clip { clip, .. } => clip.attributes(mask),
|
||||
Self::PhaseBlend(tree) => tree.attributes(mask),
|
||||
Self::BlendSpace(tree) => tree.attributes(mask),
|
||||
Self::SelectionSpace(tree) => tree
|
||||
.current()
|
||||
.ok_or("SelectionSpace attributes requested before native selection")?
|
||||
@@ -161,6 +171,7 @@ impl PlaybackTree {
|
||||
) -> Result<bool, String> {
|
||||
match self {
|
||||
Self::PhaseBlend(tree) => tree.query_attribute(name, mask, output),
|
||||
Self::BlendSpace(tree) => tree.query_attribute(name, mask, output),
|
||||
Self::SelectionSpace(tree) => tree
|
||||
.current()
|
||||
.ok_or("SelectionSpace attribute requested before native selection")?
|
||||
@@ -198,6 +209,7 @@ impl PlaybackTree {
|
||||
) -> Result<bool, String> {
|
||||
match self {
|
||||
Self::PhaseBlend(tree) => tree.evaluate(parameters, enabled, output),
|
||||
Self::BlendSpace(tree) => tree.evaluate(parameters, enabled, output),
|
||||
Self::SelectionSpace(tree) => {
|
||||
if !enabled {
|
||||
Ok(false)
|
||||
|
||||
@@ -0,0 +1,404 @@
|
||||
//! Andale BlendSpace82D22DA0..82D24B90. Authored simplex planes, original
|
||||
//! scalar selection82D23A50 and phase-coupled child clocks; no fitted metric.
|
||||
use super::{
|
||||
AdvanceResult, AnimationAttribute, AttributeName, Evaluation, PlaybackTree, PoseCommand,
|
||||
SettableAttribute,
|
||||
};
|
||||
use crate::animation::{output::attributes::MotionGraphAttribute, playback_attributes};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Simplex {
|
||||
pub children: Vec<usize>,
|
||||
pub vertices: Vec<Vec<f32>>,
|
||||
pub normals: Vec<Vec<f32>>,
|
||||
pub scales: Vec<f32>,
|
||||
}
|
||||
impl Simplex {
|
||||
///82D23898: signed distances to the opposite planes, scaled by inverse height.
|
||||
pub fn coordinates(&self, point: &[f32]) -> Vec<f32> {
|
||||
(0..self.children.len())
|
||||
.map(|i| {
|
||||
let anchor = &self.vertices[(i + 1) % self.children.len()];
|
||||
let mut even = 0.0;
|
||||
let mut odd = 0.0;
|
||||
let pairs = point.len() / 2;
|
||||
for j in 0..pairs {
|
||||
even = self.normals[i][j * 2].mul_add(point[j * 2] - anchor[j * 2], even);
|
||||
odd = self.normals[i][j * 2 + 1]
|
||||
.mul_add(point[j * 2 + 1] - anchor[j * 2 + 1], odd);
|
||||
}
|
||||
let mut dot = even + odd;
|
||||
if point.len() % 2 != 0 {
|
||||
let j = point.len() - 1;
|
||||
dot += self.normals[i][j] * (point[j] - anchor[j]);
|
||||
}
|
||||
dot * self.scales[i]
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
///82D23530/82D23080: successive coordinate-plane slices, retaining edge
|
||||
///enumeration order. On an empty slice choose its nearest remaining vertex.
|
||||
fn project(&self, point: &[f32]) -> Vec<f32> {
|
||||
let mut matrix = self.vertices.clone();
|
||||
let mut projected = Vec::with_capacity(point.len());
|
||||
for &target in point {
|
||||
let count = matrix.len() - 1;
|
||||
let mut sliced = Vec::new();
|
||||
'edges: for i in 0..count {
|
||||
for j in i + 1..matrix.len() {
|
||||
if sliced.len() == count {
|
||||
break 'edges;
|
||||
}
|
||||
let a = &matrix[i];
|
||||
let b = &matrix[j];
|
||||
if (a[0] < target) == (b[0] < target) {
|
||||
continue;
|
||||
}
|
||||
let delta = b[0] - a[0];
|
||||
if delta.abs() <= f32::from_bits(0x3727_c5ac) {
|
||||
//8219B100
|
||||
sliced.push(a[1..].to_vec());
|
||||
if sliced.len() < count {
|
||||
sliced.push(b[1..].to_vec());
|
||||
}
|
||||
} else {
|
||||
let t = (target - a[0]) / delta;
|
||||
sliced.push(
|
||||
a[1..]
|
||||
.iter()
|
||||
.zip(&b[1..])
|
||||
.map(|(&a, &b)| (b - a).mul_add(t, a))
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if sliced.is_empty() {
|
||||
let mut nearest = 0;
|
||||
let mut distance = f32::MAX;
|
||||
for (i, row) in matrix.iter().enumerate() {
|
||||
let d = (row[0] - target) * (row[0] - target);
|
||||
if d < distance {
|
||||
distance = d;
|
||||
nearest = i;
|
||||
}
|
||||
}
|
||||
projected.extend_from_slice(&matrix[nearest]);
|
||||
break;
|
||||
}
|
||||
projected.push(target);
|
||||
while sliced.len() < count {
|
||||
sliced.push(sliced.last().unwrap().clone());
|
||||
}
|
||||
matrix = sliced;
|
||||
}
|
||||
projected
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct BlendSpace {
|
||||
pub parameters: Vec<AttributeName>,
|
||||
pub children: Vec<PlaybackTree>,
|
||||
pub simplexes: Vec<Simplex>,
|
||||
values: Vec<f32>,
|
||||
current: usize,
|
||||
weights: Vec<f32>,
|
||||
pub time: f32,
|
||||
}
|
||||
impl BlendSpace {
|
||||
pub fn new(
|
||||
parameters: Vec<AttributeName>,
|
||||
children: Vec<PlaybackTree>,
|
||||
simplexes: Vec<Simplex>,
|
||||
) -> Result<Self, String> {
|
||||
let d = parameters.len();
|
||||
if d == 0
|
||||
|| d > 4
|
||||
|| children.len() < d + 1
|
||||
|| simplexes.is_empty()
|
||||
|| simplexes.iter().any(|s| {
|
||||
s.children.len() != d + 1
|
||||
|| s.vertices.len() != d + 1
|
||||
|| s.normals.len() != d + 1
|
||||
|| s.scales.len() != d + 1
|
||||
|| s.children.iter().any(|&i| i >= children.len())
|
||||
|| s.vertices
|
||||
.iter()
|
||||
.chain(&s.normals)
|
||||
.any(|v| v.len() != d || v.iter().any(|v| !v.is_finite()))
|
||||
|| s.scales.iter().any(|v| !v.is_finite())
|
||||
})
|
||||
{
|
||||
return Err("Invalid authored BlendSpace topology".into());
|
||||
}
|
||||
let mut weights = vec![0.; d + 1];
|
||||
weights[0] = 1.;
|
||||
Ok(Self {
|
||||
parameters,
|
||||
children,
|
||||
simplexes,
|
||||
values: vec![0.; d],
|
||||
current: 0,
|
||||
weights,
|
||||
time: 0.,
|
||||
})
|
||||
}
|
||||
pub fn active_weights(&self) -> impl Iterator<Item = (usize, f32)> + '_ {
|
||||
self.simplexes[self.current]
|
||||
.children
|
||||
.iter()
|
||||
.copied()
|
||||
.zip(self.weights.iter().copied())
|
||||
}
|
||||
pub fn length(&self) -> f32 {
|
||||
self.active_weights()
|
||||
.fold(0., |sum, (i, w)| w.mul_add(self.children[i].length(), sum))
|
||||
}
|
||||
pub fn set_time(&mut self, time: f32) {
|
||||
let phase = time / self.length();
|
||||
for &i in &self.simplexes[self.current].children {
|
||||
let child = &mut self.children[i];
|
||||
child.set_time(phase * child.length());
|
||||
}
|
||||
//82D24AB0 updates child clocks; the cached time is written by Advance.
|
||||
}
|
||||
pub fn set_speed(&mut self, speed: f32) {
|
||||
for child in &mut self.children {
|
||||
child.set_speed(speed);
|
||||
}
|
||||
}
|
||||
pub fn advance(&mut self, dt: f32, phase: f32, property: &mut AdvanceResult) {
|
||||
let length = self.length();
|
||||
let indices = &self.simplexes[self.current].children;
|
||||
let first = indices[0];
|
||||
let first_length = self.children[first].length();
|
||||
for (slot, &i) in indices.iter().enumerate() {
|
||||
let child = &mut self.children[i];
|
||||
let mut result = AdvanceResult {
|
||||
crossed_end: false,
|
||||
overshoot: -1.,
|
||||
remaining_before_wrap: -1.,
|
||||
};
|
||||
child.advance((child.length() * (1. / length)) * dt, phase, &mut result);
|
||||
if slot == 0 {
|
||||
*property = result;
|
||||
}
|
||||
}
|
||||
let scale = length / first_length;
|
||||
property.overshoot *= scale;
|
||||
property.remaining_before_wrap *= scale;
|
||||
self.time = self.children[first].time() * scale;
|
||||
}
|
||||
pub fn set_attributes(&mut self, attributes: &[SettableAttribute]) -> Result<bool, String> {
|
||||
let mut changed = false;
|
||||
for attribute in attributes {
|
||||
if let Some(i) = self.parameters.iter().position(|&n| n == attribute.name) {
|
||||
if !attribute.value.is_finite() {
|
||||
return Err("Nonfinite BlendSpace parameter".into());
|
||||
}
|
||||
self.values[i] = attribute.value;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if !changed {
|
||||
return Ok(false);
|
||||
}
|
||||
let mut selected = None;
|
||||
let mut closest = f32::MAX;
|
||||
for (index, simplex) in self.simplexes.iter().enumerate() {
|
||||
let mut weights = simplex.coordinates(&self.values);
|
||||
if weights.iter().all(|&w| w >= 0.) {
|
||||
selected = Some((index, weights));
|
||||
break;
|
||||
}
|
||||
weights = simplex.coordinates(&simplex.project(&self.values));
|
||||
for w in &mut weights {
|
||||
if *w < 0. {
|
||||
*w = 0.;
|
||||
}
|
||||
}
|
||||
normalize(&mut weights)?;
|
||||
let mut distance = 0.;
|
||||
for (j, &target) in self.values.iter().enumerate() {
|
||||
let projected = simplex
|
||||
.vertices
|
||||
.iter()
|
||||
.zip(&weights)
|
||||
.fold(0., |sum, (v, &w)| v[j].mul_add(w, sum));
|
||||
distance = (projected - target).mul_add(projected - target, distance);
|
||||
}
|
||||
if distance < closest {
|
||||
closest = distance;
|
||||
selected = Some((index, weights));
|
||||
}
|
||||
}
|
||||
let (index, mut weights) = selected.ok_or("BlendSpace has no finite projection")?;
|
||||
for w in &mut weights {
|
||||
*w = w.clamp(0., 1.);
|
||||
}
|
||||
normalize(&mut weights)?;
|
||||
if index != self.current {
|
||||
let old = &self.children[self.simplexes[self.current].children[0]];
|
||||
let phase = old.time() / old.length();
|
||||
for &i in &self.simplexes[index].children {
|
||||
let child = &mut self.children[i];
|
||||
child.set_time(phase * child.length());
|
||||
}
|
||||
self.current = index;
|
||||
}
|
||||
self.weights = weights;
|
||||
Ok(true)
|
||||
}
|
||||
pub fn attributes(&self, mask: u32) -> Result<Vec<AnimationAttribute>, String> {
|
||||
let indices = &self.simplexes[self.current].children;
|
||||
let mut output = self.children[indices[0]].attributes(mask)?;
|
||||
for a in &mut output {
|
||||
playback_attributes::scale(a, self.weights[0])?;
|
||||
}
|
||||
for (slot, &i) in indices.iter().enumerate().skip(1) {
|
||||
let right = self.children[i].attributes(mask)?;
|
||||
let mut next = 0;
|
||||
let mut combined = Vec::new();
|
||||
for mut a in output {
|
||||
while next < right.len() && right[next].name.0 < a.name.0 {
|
||||
next += 1;
|
||||
}
|
||||
if let Some(b) = right.get(next).filter(|b| b.name == a.name) {
|
||||
playback_attributes::add_weighted(&mut a, b, self.weights[slot])?;
|
||||
combined.push(a);
|
||||
}
|
||||
}
|
||||
output = combined;
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
pub fn query_attribute(
|
||||
&self,
|
||||
name: AttributeName,
|
||||
mask: u32,
|
||||
output: &mut AnimationAttribute,
|
||||
) -> Result<bool, String> {
|
||||
//82D24580 indexes the first d+1 authored children directly (unlike
|
||||
//GetAttributes, which follows the active simplex). Preserve that distinction.
|
||||
if !self.children[0].query_attribute(name, mask, output)? {
|
||||
return Ok(false);
|
||||
}
|
||||
playback_attributes::scale(output, self.weights[0])?;
|
||||
for i in 1..self.weights.len() {
|
||||
let mut right = MotionGraphAttribute { name, value: 0. }.to_animation();
|
||||
if !self.children[i].query_attribute(name, 15, &mut right)? {
|
||||
return Ok(false);
|
||||
}
|
||||
playback_attributes::add_weighted(output, &right, self.weights[i])?;
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
pub fn evaluate(
|
||||
&mut self,
|
||||
parameters: Evaluation,
|
||||
enabled: bool,
|
||||
output: &mut Vec<PoseCommand>,
|
||||
) -> Result<bool, String> {
|
||||
if !enabled {
|
||||
return Ok(false);
|
||||
}
|
||||
for &i in &self.simplexes[self.current].children {
|
||||
if !self.children[i].evaluate(parameters, true, output)? {
|
||||
return Err("BlendSpace child produced no pose".into());
|
||||
}
|
||||
}
|
||||
output.push(PoseCommand::WeightedBlend {
|
||||
weights: self.weights.clone(),
|
||||
});
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
fn normalize(weights: &mut [f32]) -> Result<(), String> {
|
||||
let sum: f32 = weights.iter().sum();
|
||||
if !sum.is_finite() || sum <= 0. {
|
||||
return Err("Degenerate BlendSpace weights".into());
|
||||
}
|
||||
let inverse = 1. / sum;
|
||||
for w in weights {
|
||||
*w *= inverse;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::animation::{playback_clip::PlaybackClip, skeleton_input::name::encode};
|
||||
fn triangle() -> Simplex {
|
||||
Simplex {
|
||||
children: vec![0, 1, 2],
|
||||
vertices: vec![vec![0., 0.], vec![1., 0.], vec![0., 1.]],
|
||||
normals: vec![vec![-1., -1.], vec![1., 0.], vec![0., 1.]],
|
||||
scales: vec![1.; 3],
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn bump_space_planes_and_original_outside_projection() {
|
||||
let s = triangle();
|
||||
assert_eq!(s.coordinates(&[0.25, 0.5]), vec![0.25, 0.25, 0.5]);
|
||||
assert_eq!(s.project(&[2., 0.5]), vec![1., 0.]);
|
||||
assert_eq!(s.project(&[0.5, 2.]), vec![0.5, 0.5]);
|
||||
assert_eq!(s.project(&[-1., -1.]), vec![0., 0.]);
|
||||
}
|
||||
#[test]
|
||||
fn bump_space_clocks_share_phase_and_keep_all_weighted_poses() {
|
||||
let children = (1..=3)
|
||||
.map(|i| PlaybackTree::Clip {
|
||||
name: format!("CLIP{i}"),
|
||||
clip: PlaybackClip::new((i * 30 + 1) as f32, 30., 1., 0, Vec::new()),
|
||||
})
|
||||
.collect();
|
||||
let x = encode(b"X");
|
||||
let y = encode(b"Y");
|
||||
let mut tree = BlendSpace::new(vec![x, y], children, vec![triangle()]).unwrap();
|
||||
tree.set_attributes(&[
|
||||
SettableAttribute {
|
||||
name: x,
|
||||
value: 0.25,
|
||||
normalized: false,
|
||||
sequence_id: -1,
|
||||
},
|
||||
SettableAttribute {
|
||||
name: y,
|
||||
value: 0.5,
|
||||
normalized: false,
|
||||
sequence_id: -1,
|
||||
},
|
||||
])
|
||||
.unwrap();
|
||||
let length = tree.length();
|
||||
tree.set_time(length * 0.2);
|
||||
let mut property = AdvanceResult {
|
||||
crossed_end: false,
|
||||
overshoot: -1.,
|
||||
remaining_before_wrap: -1.,
|
||||
};
|
||||
tree.advance(length * 0.1, 0., &mut property);
|
||||
for child in &tree.children {
|
||||
assert!((child.time() / child.length() - 0.3).abs() < 1e-6);
|
||||
}
|
||||
let mut commands = Vec::new();
|
||||
tree.evaluate(
|
||||
Evaluation {
|
||||
cull_threshold: 0.01,
|
||||
update_history: true,
|
||||
},
|
||||
true,
|
||||
&mut commands,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(commands.len(), 4);
|
||||
assert_eq!(
|
||||
commands.last(),
|
||||
Some(&PoseCommand::WeightedBlend {
|
||||
weights: vec![0.25, 0.25, 0.5]
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,39 @@ fn interpolate(first: [f32; 4], second: [f32; 4], weight: f32) -> [f32; 4] {
|
||||
core::array::from_fn(|lane| (second[lane] - first[lane]).mul_add(weight, first[lane]))
|
||||
}
|
||||
|
||||
///ACS828CBF68..828CC0B4: weighted SQT accumulation in authored order,
|
||||
///then one quaternion normalization. This is distinct from repeated two-pose
|
||||
///shortest-arc blending; the native multi-pose path does not flip hemispheres.
|
||||
pub fn weighted(poses: &[Vec<Sqt>], weights: &[f32]) -> Result<Vec<Sqt>, PoseBufferError> {
|
||||
let Some(first) = poses.first() else {return Err(PoseBufferError::ShortInput);};
|
||||
if poses.len()!=weights.len() || poses.iter().any(|p| p.len()!=first.len()) {
|
||||
return Err(PoseBufferError::ShortInput);
|
||||
}
|
||||
let mut output=first.iter().map(|s| Sqt {
|
||||
scale:s.scale.map(|v| v*weights[0]),
|
||||
rotation:s.rotation.map(|v| v*weights[0]),
|
||||
translation:s.translation.map(|v| v*weights[0]),
|
||||
}).collect::<Vec<_>>();
|
||||
for (pose,&weight) in poses.iter().zip(weights).skip(1) {
|
||||
for (out,s) in output.iter_mut().zip(pose) {
|
||||
for lane in 0..4 {
|
||||
out.scale[lane]=s.scale[lane].mul_add(weight,out.scale[lane]);
|
||||
out.rotation[lane]=s.rotation[lane].mul_add(weight,out.rotation[lane]);
|
||||
out.translation[lane]=s.translation[lane].mul_add(weight,out.translation[lane]);
|
||||
}
|
||||
}
|
||||
}
|
||||
for out in &mut output {
|
||||
let squared=native_arithmetic::dot4(out.rotation,out.rotation);
|
||||
let mut inverse=native_arithmetic::reciprocal_square_root_estimate(squared);
|
||||
for _ in 0..2 {
|
||||
inverse=(inverse*0.5).mul_add((-squared).mul_add(inverse*inverse,1.),inverse);
|
||||
}
|
||||
out.rotation=out.rotation.map(|v| v*inverse);
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/pose_blend.rs"]
|
||||
mod tests;
|
||||
@@ -1,5 +1,18 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn bump_multi_blend_accumulates_before_normalizing_without_hemisphere_flip() {
|
||||
let a=pose([0.,0.,0.,1.]);
|
||||
let mut b=pose([0.6,0.,0.,-0.8]);
|
||||
b.translation=[4.,8.,12.,0.];
|
||||
let c=pose([0.,0.6,0.,0.8]);
|
||||
let output=weighted(&[vec![a],vec![b],vec![c]],&[0.5,0.25,0.25]).unwrap()[0];
|
||||
assert_eq!(output.translation,[1.,2.,3.,0.]);
|
||||
let raw=[0.15_f32,0.15,0.,0.5];
|
||||
let norm=raw.iter().map(|v| v*v).sum::<f32>().sqrt();
|
||||
for i in 0..4 {assert!((output.rotation[i]-raw[i]/norm).abs()<1e-6);}
|
||||
}
|
||||
|
||||
fn pose(rotation: [f32; 4]) -> Sqt {
|
||||
Sqt {
|
||||
scale: [1.0; 4],
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
use serde::Deserialize;
|
||||
use std::{collections::BTreeMap, fs, path::Path};
|
||||
mod native;
|
||||
mod blend_space;
|
||||
pub use blend_space::{BlendSpaceMetadata, BlendSimplexMetadata};
|
||||
mod selection_space;
|
||||
pub use selection_space::{
|
||||
SelectionCandidateMetadata, SelectionParameterMetadata, SelectionSpaceMetadata,
|
||||
@@ -52,6 +54,7 @@ pub struct PhaseBlendMetadata {
|
||||
}
|
||||
pub enum TreeMetadata<'a> {
|
||||
Clip(&'a ClipMetadata),
|
||||
BlendSpace(&'a BlendSpaceMetadata),
|
||||
PhaseBlend(&'a PhaseBlendMetadata),
|
||||
Selector(&'a SelectorMetadata),
|
||||
SelectionSpace(&'a SelectionSpaceMetadata),
|
||||
@@ -86,6 +89,8 @@ struct File {
|
||||
#[serde(default)]
|
||||
phase_blends: Vec<PhaseBlendMetadata>,
|
||||
#[serde(default)]
|
||||
blend_spaces: Vec<BlendSpaceMetadata>,
|
||||
#[serde(default)]
|
||||
selectors: Vec<SelectorMetadata>,
|
||||
#[serde(default)]
|
||||
selection_spaces: Vec<SelectionSpaceMetadata>,
|
||||
@@ -98,6 +103,7 @@ pub struct AnimationMetadata {
|
||||
pub source_sha256: String,
|
||||
clips: BTreeMap<String, Vec<ClipMetadata>>,
|
||||
phase_blends: BTreeMap<String, Vec<PhaseBlendMetadata>>,
|
||||
blend_spaces: BTreeMap<String, Vec<BlendSpaceMetadata>>,
|
||||
selectors: BTreeMap<String, Vec<SelectorMetadata>>,
|
||||
selection_spaces: BTreeMap<String, Vec<SelectionSpaceMetadata>>,
|
||||
unsupported: BTreeMap<String, Vec<(u64, u32)>>,
|
||||
@@ -191,6 +197,11 @@ impl AnimationMetadata {
|
||||
.or_default()
|
||||
.push(tree);
|
||||
}
|
||||
let mut blend_spaces: BTreeMap<String, Vec<BlendSpaceMetadata>> = BTreeMap::new();
|
||||
for tree in file.blend_spaces {
|
||||
tree.validate(file.source_bytes)?;
|
||||
blend_spaces.entry(tree.name.clone()).or_default().push(tree);
|
||||
}
|
||||
let mut selectors: BTreeMap<String, Vec<SelectorMetadata>> = BTreeMap::new();
|
||||
for tree in file.selectors {
|
||||
validate_name(&tree.name, 36)?;
|
||||
@@ -231,7 +242,7 @@ impl AnimationMetadata {
|
||||
}
|
||||
let origins = clips
|
||||
.keys()
|
||||
.chain(phase_blends.keys())
|
||||
.chain(phase_blends.keys()).chain(blend_spaces.keys())
|
||||
.chain(selectors.keys())
|
||||
.chain(selection_spaces.keys())
|
||||
.chain(unsupported.keys())
|
||||
@@ -247,6 +258,7 @@ impl AnimationMetadata {
|
||||
source_sha256: file.source_sha256,
|
||||
clips,
|
||||
phase_blends,
|
||||
blend_spaces,
|
||||
selectors,
|
||||
selection_spaces,
|
||||
unsupported,
|
||||
@@ -276,6 +288,7 @@ impl AnimationMetadata {
|
||||
self.sources.extend(other.sources);
|
||||
self.clips.extend(other.clips);
|
||||
self.phase_blends.extend(other.phase_blends);
|
||||
self.blend_spaces.extend(other.blend_spaces);
|
||||
self.selectors.extend(other.selectors);
|
||||
self.selection_spaces.extend(other.selection_spaces);
|
||||
self.unsupported.extend(other.unsupported);
|
||||
@@ -319,6 +332,7 @@ impl AnimationMetadata {
|
||||
.get(&name)
|
||||
.map(Vec::as_slice)
|
||||
.unwrap_or(&[]);
|
||||
let blend_spaces = self.blend_spaces.get(&name).map(Vec::as_slice).unwrap_or(&[]);
|
||||
let selectors = self.selectors.get(&name).map(Vec::as_slice).unwrap_or(&[]);
|
||||
let selection_spaces = self
|
||||
.selection_spaces
|
||||
@@ -330,7 +344,7 @@ impl AnimationMetadata {
|
||||
let last = clips
|
||||
.iter()
|
||||
.map(|c| c.source_offset)
|
||||
.chain(phase_blends.iter().map(|t| t.source_offset))
|
||||
.chain(phase_blends.iter().map(|t| t.source_offset)).chain(blend_spaces.iter().map(|t| t.source_offset))
|
||||
.chain(selectors.iter().map(|t| t.source_offset))
|
||||
.chain(selection_spaces.iter().map(|t| t.source_offset))
|
||||
.chain(trees.iter().map(|t| t.0))
|
||||
@@ -341,6 +355,9 @@ impl AnimationMetadata {
|
||||
if let Some(tree) = phase_blends.iter().find(|t| Some(t.source_offset) == last) {
|
||||
return Ok(TreeMetadata::PhaseBlend(tree));
|
||||
}
|
||||
if let Some(tree) = blend_spaces.iter().find(|t| Some(t.source_offset) == last) {
|
||||
return Ok(TreeMetadata::BlendSpace(tree));
|
||||
}
|
||||
if let Some(tree) = selectors.iter().find(|t| Some(t.source_offset) == last) {
|
||||
return Ok(TreeMetadata::Selector(tree));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
//! Andale type6: construction82D1B8D0, layout82D22DA0, simplex82D23898.
|
||||
use super::validate_name;
|
||||
use crate::abin::{Error, Reader, RecordHeader, Result};
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct BlendSimplexMetadata {
|
||||
pub children: Vec<usize>,
|
||||
/// Spatial coordinates only; the serialized matrix also has a final column.
|
||||
pub vertex_bits: Vec<Vec<u32>>,
|
||||
pub normal_bits: Vec<Vec<u32>>,
|
||||
pub scale_bits: Vec<u32>,
|
||||
}
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct BlendSpaceMetadata {
|
||||
pub name: String,
|
||||
pub source_offset: u64,
|
||||
pub parameters: Vec<String>,
|
||||
pub children: Vec<String>,
|
||||
pub simplexes: Vec<BlendSimplexMetadata>,
|
||||
}
|
||||
impl BlendSpaceMetadata {
|
||||
pub(super) fn validate(&self, bytes: u64) -> std::result::Result<(), String> {
|
||||
validate_name(&self.name, 36)?;
|
||||
let d = self.parameters.len();
|
||||
if self.source_offset >= bytes
|
||||
|| d == 0
|
||||
|| d > 4
|
||||
|| self.children.len() < d + 1
|
||||
|| self.simplexes.is_empty()
|
||||
{
|
||||
return Err(format!(
|
||||
"{}: invalid BlendSpace dimensions/offset",
|
||||
self.name
|
||||
));
|
||||
}
|
||||
for name in &self.parameters {
|
||||
validate_name(name, 30)?;
|
||||
}
|
||||
for name in &self.children {
|
||||
validate_name(name, 36)?;
|
||||
}
|
||||
for s in &self.simplexes {
|
||||
if s.children.len() != d + 1
|
||||
|| s.scale_bits.len() != d + 1
|
||||
|| s.vertex_bits.len() != d + 1
|
||||
|| s.normal_bits.len() != d + 1
|
||||
|| s.children.iter().any(|&i| i >= self.children.len())
|
||||
|| s.vertex_bits
|
||||
.iter()
|
||||
.chain(&s.normal_bits)
|
||||
.any(|v| v.len() != d)
|
||||
|| s.vertex_bits
|
||||
.iter()
|
||||
.chain(&s.normal_bits)
|
||||
.flatten()
|
||||
.chain(&s.scale_bits)
|
||||
.any(|&v| !f32::from_bits(v).is_finite())
|
||||
{
|
||||
return Err(format!("{}: invalid BlendSpace simplex", self.name));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
pub(super) fn read(r: Reader<'_>, h: &RecordHeader) -> Result<BlendSpaceMetadata> {
|
||||
let p = h.payload_offset;
|
||||
let count = r.u32(p)? as usize;
|
||||
let names_size = count
|
||||
.checked_mul(24)
|
||||
.ok_or_else(|| Error::new(p, "BlendSpace count overflow"))?;
|
||||
let names = r.relative(p, r.u32(p + 4)?, names_size)?.start;
|
||||
let q = r.relative(p, r.u32(p + 12)?, 44)?.start;
|
||||
let space = r.bounded(r.relative(q, 0, r.u32(q)? as usize)?)?;
|
||||
let d = space.u32(q + 28)? as usize;
|
||||
if d == 0 || d > 4 || d > r.u32(p + 16)? as usize {
|
||||
return Err(Error::new(q + 28, "invalid BlendSpace dimension"));
|
||||
}
|
||||
let params = space.relative(q, space.u32(q + 32)?, d * 20)?.start;
|
||||
let simplex_count = space.u32(q + 36)? as usize;
|
||||
// Every record must at least contain its header; bound the count before allocating.
|
||||
space.relative(
|
||||
q,
|
||||
space.u32(q + 40)?,
|
||||
simplex_count
|
||||
.checked_mul(20)
|
||||
.ok_or_else(|| Error::new(q, "BlendSpace simplex count overflow"))?,
|
||||
)?;
|
||||
let mut at = space.relative(q, space.u32(q + 40)?, 20)?.start;
|
||||
let mut simplexes = Vec::new();
|
||||
for _ in 0..simplex_count {
|
||||
let size = space.u32(at)? as usize;
|
||||
let s = space.bounded(space.relative(at, 0, size)?)?;
|
||||
if s.u32(at + 16)? as usize != d + 1 {
|
||||
return Err(Error::new(at + 16, "BlendSpace matrix dimension mismatch"));
|
||||
}
|
||||
s.take(at + 20, (d + 1) * (d + 1) * 4)?;
|
||||
let indices = s.relative(at, s.u32(at + 4)?, (d + 1) * 4)?.start;
|
||||
let scales = s.relative(at, s.u32(at + 8)?, (d + 1) * 4)?.start;
|
||||
let normals = s
|
||||
.relative(at, s.u32(at + 12)?, (d + 1) * (d + 1) * 4)?
|
||||
.start;
|
||||
let mut simplex = BlendSimplexMetadata {
|
||||
children: Vec::new(),
|
||||
vertex_bits: Vec::new(),
|
||||
normal_bits: Vec::new(),
|
||||
scale_bits: Vec::new(),
|
||||
};
|
||||
for i in 0..=d {
|
||||
let normal = normals + i * (d + 1) * 4;
|
||||
if s.u32(normal)? as usize != d {
|
||||
return Err(Error::new(normal, "BlendSpace normal dimension mismatch"));
|
||||
}
|
||||
simplex.children.push(s.u32(indices + i * 4)? as usize);
|
||||
simplex.scale_bits.push(s.u32(scales + i * 4)?);
|
||||
simplex.vertex_bits.push(
|
||||
(0..d)
|
||||
.map(|j| s.u32(at + 20 + (i * (d + 1) + j) * 4))
|
||||
.collect::<Result<_>>()?,
|
||||
);
|
||||
simplex.normal_bits.push(
|
||||
(0..d)
|
||||
.map(|j| s.u32(normal + 4 + j * 4))
|
||||
.collect::<Result<_>>()?,
|
||||
);
|
||||
}
|
||||
simplexes.push(simplex);
|
||||
at += size;
|
||||
}
|
||||
Ok(BlendSpaceMetadata {
|
||||
name: h.name.clone(),
|
||||
source_offset: h.offset as u64,
|
||||
parameters: (0..d)
|
||||
.map(|i| space.name(params + i * 20, 5))
|
||||
.collect::<Result<_>>()?,
|
||||
children: (0..count)
|
||||
.map(|i| r.name(names + i * 24, 6))
|
||||
.collect::<Result<_>>()?,
|
||||
simplexes,
|
||||
})
|
||||
}
|
||||
@@ -21,6 +21,7 @@ impl AnimationMetadata {
|
||||
source_bytes: bank.bytes().len() as u64,
|
||||
clips: Vec::new(),
|
||||
phase_blends: Vec::new(),
|
||||
blend_spaces: Vec::new(),
|
||||
selectors: Vec::new(),
|
||||
selection_spaces: Vec::new(),
|
||||
unsupported_trees: Vec::new(),
|
||||
@@ -35,6 +36,7 @@ impl AnimationMetadata {
|
||||
.push(read_clip(r, h, clip).map_err(|e| e.to_string())?),
|
||||
RecordData::Pose(_) | RecordData::Hierarchy(_) | RecordData::PhysicsPose(_) => {}
|
||||
RecordData::Opaque => match h.type_id {
|
||||
6 => file.blend_spaces.push(super::blend_space::read(r, h).map_err(|e| e.to_string())?),
|
||||
7 => file
|
||||
.phase_blends
|
||||
.push(read_phase_blend(r, h).map_err(|e| e.to_string())?),
|
||||
|
||||
@@ -95,6 +95,12 @@ impl PoseEvaluator {
|
||||
}
|
||||
stack.push(pose);
|
||||
}
|
||||
PoseCommand::WeightedBlend { weights } => {
|
||||
let start=stack.len().checked_sub(weights.len()).ok_or("Weighted blend pose stack underflow")?;
|
||||
let pose=skate_core::animation::pose_blend::weighted(&stack[start..],weights)
|
||||
.map_err(|e| format!("Weighted blend: {e:?}"))?;
|
||||
stack.truncate(start); stack.push(pose);
|
||||
}
|
||||
PoseCommand::Blend { weight } | PoseCommand::ChannelBlend { weight, .. } => {
|
||||
let second = stack.pop().ok_or("Animation blend has no second subtree")?;
|
||||
let first = stack
|
||||
|
||||
@@ -151,6 +151,8 @@ pub struct MotionHost {
|
||||
wipeout_settings: super::motion_wipeout::Settings,
|
||||
///Native PhysOutGround pumping acceleration268.
|
||||
pub pumping_acceleration: Option<f32>,
|
||||
pub bump_acceleration: Option<[f32;4]>,
|
||||
bump_settings: skate_core::animation::bump::Settings,
|
||||
pub allow_pumping: bool,
|
||||
pub riding: super::motion_riding::RidingState,
|
||||
pub errors: Vec<String>,
|
||||
@@ -236,6 +238,13 @@ impl MotionHost {
|
||||
wipeout_controls: Default::default(),
|
||||
wipeout_settings: super::motion_wipeout::Settings::load(data)?,
|
||||
pumping_acceleration: None,
|
||||
bump_acceleration: None,
|
||||
bump_settings: skate_core::animation::bump::Settings {
|
||||
scale_x_acc:data.float("anim_motion","bumps","scale_x_acc")?,
|
||||
min_bump_mag:data.float("anim_motion","bumps","min_bump_mag")?,
|
||||
min_bump_blend_value:data.float("anim_motion","bumps","min_bump_blend_value")?,
|
||||
max_bump_mag:data.float("anim_motion","bumps","max_bump_mag")?,
|
||||
},
|
||||
//8258F488 seeds bit24, and reset825953B0 preserves that bit.
|
||||
allow_pumping: true,
|
||||
riding: super::motion_riding::RidingState::new(),
|
||||
@@ -359,3 +368,7 @@ mod tests;
|
||||
#[cfg(test)]
|
||||
#[path = "tests/motion_slide.rs"]
|
||||
mod slide_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/motion_bump.rs"]
|
||||
mod bump_tests;
|
||||
@@ -286,6 +286,7 @@ fn has_transition(tree: &PlaybackTree) -> bool {
|
||||
match tree {
|
||||
PlaybackTree::Transition(_) => true,
|
||||
PlaybackTree::PhaseBlend(tree) => tree.children.iter().any(has_transition),
|
||||
PlaybackTree::BlendSpace(tree) => tree.children.iter().any(has_transition),
|
||||
PlaybackTree::SelectionSpace(tree) => tree.current().is_some_and(has_transition),
|
||||
PlaybackTree::BindPose { motion, .. } => has_transition(motion),
|
||||
_ => false,
|
||||
@@ -306,6 +307,10 @@ fn prune(tree: PlaybackTree) -> PlaybackTree {
|
||||
tree.children = tree.children.into_iter().map(prune).collect();
|
||||
PlaybackTree::PhaseBlend(tree)
|
||||
}
|
||||
PlaybackTree::BlendSpace(mut tree) => {
|
||||
tree.children = tree.children.into_iter().map(prune).collect();
|
||||
PlaybackTree::BlendSpace(tree)
|
||||
}
|
||||
PlaybackTree::SelectionSpace(mut tree) => {
|
||||
tree.candidates = tree
|
||||
.candidates
|
||||
|
||||
@@ -28,6 +28,10 @@ impl MotionAnimation {
|
||||
}
|
||||
self.prepare_selection_spaces(space.current_mut().unwrap(), attributes)?;
|
||||
}
|
||||
PlaybackTree::BlendSpace(space) => {
|
||||
// Type6 SetAttributes82D24078 consumes its own parameters only.
|
||||
let _ = space;
|
||||
}
|
||||
PlaybackTree::PhaseBlend(space) => {
|
||||
for child in &mut space.children {
|
||||
self.prepare_selection_spaces(child, attributes)?;
|
||||
|
||||
@@ -33,6 +33,19 @@ pub(super) fn build(
|
||||
.collect(),
|
||||
),
|
||||
},
|
||||
TreeMetadata::BlendSpace(source) => {
|
||||
use skate_core::animation::playback_tree::blend_space::{BlendSpace, Simplex};
|
||||
PlaybackTree::BlendSpace(BlendSpace::new(
|
||||
source.parameters.iter().map(|p| encode(p.as_bytes())).collect(),
|
||||
source.children.iter().map(|child| build(metadata, child, construction, parents)).collect::<Result<_,_>>()?,
|
||||
source.simplexes.iter().map(|s| Simplex {
|
||||
children:s.children.clone(),
|
||||
vertices:s.vertex_bits.iter().map(|v| v.iter().map(|&v| f32::from_bits(v)).collect()).collect(),
|
||||
normals:s.normal_bits.iter().map(|v| v.iter().map(|&v| f32::from_bits(v)).collect()).collect(),
|
||||
scales:s.scale_bits.iter().map(|&v| f32::from_bits(v)).collect(),
|
||||
}).collect(),
|
||||
)?)
|
||||
}
|
||||
TreeMetadata::PhaseBlend(source) => PlaybackTree::PhaseBlend(PhaseBlend::new(
|
||||
encode(source.parameter.as_bytes()),
|
||||
source
|
||||
|
||||
@@ -89,6 +89,18 @@ impl MotionHost {
|
||||
//Retail ctor82BA58E8 retains only diagnostic text/layout. All three
|
||||
//lifecycle slots in82309664 are82B61BB8 (blr), with no state writes.
|
||||
(MotionOperation::PrintText2D, _) => {}
|
||||
(MotionOperation::SetBumpCoefficients(names), _) => {
|
||||
// Begin only: vtable823200C8+48; Update/End are82B61BB8.
|
||||
if phase == 0 {
|
||||
use skate_core::animation::playback_parameters::{AttributeSink, SettableAttribute};
|
||||
let acceleration=self.bump_acceleration.ok_or("SetBumpCoefficients requires PhysOutAnimation112")?;
|
||||
let flags=self.animation.skater_animation_flags.ok_or("SetBumpCoefficients requires live ISkaterAnim stance")?;
|
||||
let values=skate_core::animation::bump::coefficients(acceleration,flags & 0x4000_0000 != 0,&self.bump_settings);
|
||||
for (name,value) in names.into_iter().zip(values) {
|
||||
AttributeSink::set_attribute(&mut self.animation,SettableAttribute {name,value,normalized:false,sequence_id:-1});
|
||||
}
|
||||
}
|
||||
}
|
||||
(MotionOperation::Shove(operation), Instance::Shove(state)) => match phase {
|
||||
0 => state.begin(),
|
||||
1 => state.update(
|
||||
|
||||
@@ -25,6 +25,7 @@ pub enum MotionOperation {
|
||||
Push(PushOperation),
|
||||
/// Angle, direction, quickness, speed, holding, in native emission order.
|
||||
SetTurning([AttributeName; 5]),
|
||||
SetBumpCoefficients([AttributeName; 2]),
|
||||
ApplyingBodyTilt,
|
||||
SettingBodyTilt(AttributeName),
|
||||
Crouching(AttributeName),
|
||||
@@ -93,6 +94,7 @@ impl OperationFactory for MotionFactory {
|
||||
Some(MotionOperation::Shove(operation))
|
||||
} else {
|
||||
match name {
|
||||
"SetBumpCoefficients" => Some(MotionOperation::SetBumpCoefficients([key(a,"X",""),key(a,"Y","")])),
|
||||
"ResetSkaterAnimation" | "ResetToGivenStance" => Some(MotionOperation::ResetAnimation(
|
||||
super::motion_reset::Operation::parse(a).ok_or("Invalid reset operation")?,
|
||||
)),
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
use super::*;
|
||||
use skate_core::animation::{
|
||||
output::Sqt,
|
||||
playback_tree::{Evaluation, PlaybackTree},
|
||||
};
|
||||
|
||||
fn finite(pose: &[Sqt]) {
|
||||
assert!(!pose.is_empty());
|
||||
for (i, bone) in pose.iter().enumerate() {
|
||||
assert!(
|
||||
bone.scale
|
||||
.iter()
|
||||
.chain(&bone.rotation)
|
||||
.chain(&bone.translation)
|
||||
.all(|v| v.is_finite()),
|
||||
"nonfinite bump bone {i}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires the user's private stock animation and graph assets"]
|
||||
fn stock_bump_crash_regression() {
|
||||
use crate::{animation_pose::PoseEvaluator, graph_runtime::CompiledGraph};
|
||||
use skate_data::{
|
||||
animation_banks::AnimationBanks,
|
||||
state_graph::{StateGraph, binding::Binding},
|
||||
};
|
||||
let root = std::path::PathBuf::from(
|
||||
std::env::var_os("SKATE3_ASSET_ROOT").expect("Set SKATE3_ASSET_ROOT"),
|
||||
);
|
||||
let banks = AnimationBanks::load(&root).unwrap();
|
||||
let evaluator = PoseEvaluator::from_banks(&banks).unwrap();
|
||||
let source =
|
||||
StateGraph::load(&root.join("private/stock/data/state/MotionGraph_OnBoard.stategraph"))
|
||||
.unwrap();
|
||||
let binding = Binding::from_graph(&source).unwrap();
|
||||
let runtime = CompiledGraph::from_binding(&binding).unwrap();
|
||||
let graph = LoadedGraph {
|
||||
source,
|
||||
binding,
|
||||
runtime,
|
||||
};
|
||||
let collections = Collections::load(&root).unwrap();
|
||||
let mut host = MotionHost::from_graph(
|
||||
&graph,
|
||||
&collections,
|
||||
banks.metadata().unwrap(),
|
||||
PlaybackContext {
|
||||
is_switch: Some(false),
|
||||
is_mirrored: Some(false),
|
||||
board_available: Some(true),
|
||||
pro_skater: encode(b""),
|
||||
transition_override: None,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
host.animation
|
||||
.set_hierarchy(
|
||||
&evaluator.frames.bone_names,
|
||||
&evaluator.frames.mirror_indices,
|
||||
)
|
||||
.unwrap();
|
||||
let mut tree = host.animation.build_tree("B_BUMP").unwrap();
|
||||
let PlaybackTree::BlendSpace(space) = &tree else {
|
||||
panic!("B_BUMP lost its type6 topology")
|
||||
};
|
||||
assert_eq!(
|
||||
space.parameters,
|
||||
vec![encode(b"DISTTOCOG"), encode(b"BUMPX"), encode(b"BUMPY")]
|
||||
);
|
||||
assert_eq!(space.children.len(), 26);
|
||||
assert_eq!(space.simplexes.len(), 48);
|
||||
let parameters = space.parameters.clone();
|
||||
let points = space
|
||||
.simplexes
|
||||
.iter()
|
||||
.flat_map(|s| {
|
||||
let center = (0..3)
|
||||
.map(|j| s.vertices.iter().map(|v| v[j]).sum::<f32>() / 4.)
|
||||
.collect();
|
||||
s.vertices
|
||||
.iter()
|
||||
.cloned()
|
||||
.chain(std::iter::once(center))
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.chain([vec![0., 0., 0.], vec![2., 2., -2.], vec![0.8, 0., 0.]])
|
||||
.collect::<Vec<_>>();
|
||||
let evaluation = Evaluation {
|
||||
cull_threshold: 0.01,
|
||||
update_history: true,
|
||||
};
|
||||
//Decode every source clip and evaluate authored vertices/interiors. No physics or gameplay automation.
|
||||
for child in &space.children {
|
||||
let mut child = child.clone();
|
||||
child.set_time(child.length() * 0.4);
|
||||
let mut commands = Vec::new();
|
||||
child.evaluate(evaluation, true, &mut commands).unwrap();
|
||||
finite(&evaluator.evaluate(&commands).unwrap());
|
||||
}
|
||||
for point in &points {
|
||||
tree.set_attributes(
|
||||
¶meters
|
||||
.iter()
|
||||
.zip(point)
|
||||
.map(|(&name, &value)| SettableAttribute {
|
||||
name,
|
||||
value,
|
||||
normalized: false,
|
||||
sequence_id: -1,
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.unwrap();
|
||||
tree.set_time(tree.length() * 0.4);
|
||||
let mut commands = Vec::new();
|
||||
tree.evaluate(evaluation, true, &mut commands).unwrap();
|
||||
let pose = evaluator.evaluate(&commands).unwrap();
|
||||
finite(&pose);
|
||||
for bone in &pose {
|
||||
let norm: f32 = bone.rotation.iter().map(|v| v * v).sum();
|
||||
assert!((norm - 1.).abs() < 1e-5, "weighted quaternion norm {norm}");
|
||||
}
|
||||
tree.attributes(15).unwrap();
|
||||
let PlaybackTree::BlendSpace(space) = &tree else {
|
||||
unreachable!()
|
||||
};
|
||||
let weights = space.active_weights().map(|(_, w)| w).collect::<Vec<_>>();
|
||||
assert!(weights.iter().all(|&w| w >= 0. && w <= 1.));
|
||||
if points[..240].iter().any(|p| p == point) {
|
||||
let simplex = space
|
||||
.simplexes
|
||||
.iter()
|
||||
.find(|s| {
|
||||
s.children
|
||||
.iter()
|
||||
.copied()
|
||||
.eq(space.active_weights().map(|(i, _)| i))
|
||||
})
|
||||
.unwrap();
|
||||
for j in 0..3 {
|
||||
let actual = simplex
|
||||
.vertices
|
||||
.iter()
|
||||
.zip(&weights)
|
||||
.map(|(v, w)| v[j] * w)
|
||||
.sum::<f32>();
|
||||
assert!(
|
||||
(actual - point[j]).abs() < 2e-5,
|
||||
"authored point {point:?} reconstructed as {actual} on axis {j}"
|
||||
);
|
||||
}
|
||||
}
|
||||
assert!((weights.iter().sum::<f32>() - 1.).abs() < 1e-6);
|
||||
}
|
||||
//Exact runtime behavior IDs in the user's crash log.
|
||||
assert!(
|
||||
matches!(&host.operations[host.remap.behaviors[3623]], MotionOperation::Play(p) if p.animation.eq_ignore_ascii_case("B_BUMP"))
|
||||
);
|
||||
assert!(
|
||||
matches!(&host.operations[host.remap.behaviors[3624]], MotionOperation::SetBumpCoefficients(names) if *names==[encode(b"BUMPX"),encode(b"BUMPY")])
|
||||
);
|
||||
let frame = Frame {
|
||||
dt: 1. / 60.,
|
||||
current: None,
|
||||
last: None,
|
||||
state_times: Vec::new(),
|
||||
};
|
||||
for mirrored in [false, true] {
|
||||
host.animation.reset_from_stock();
|
||||
host.animation.skater_animation_flags = Some(if mirrored { 0x4000_0000 } else { 0 });
|
||||
host.bump_acceleration = Some([150., 0., -150., 0.]);
|
||||
host.execute(3623, &frame, 0).unwrap();
|
||||
host.execute(3624, &frame, 0).unwrap();
|
||||
host.animation.apply_parameters().unwrap();
|
||||
host.animation.advance(1. / 60., 0.);
|
||||
host.animation.refresh_tree_attributes().unwrap();
|
||||
finite(
|
||||
&evaluator
|
||||
.evaluate(&host.animation.evaluate_pose(evaluation).unwrap())
|
||||
.unwrap(),
|
||||
);
|
||||
//Update/End must not recapture the impact.
|
||||
host.bump_acceleration = None;
|
||||
host.execute(3624, &frame, 1).unwrap();
|
||||
host.execute(3624, &frame, 2).unwrap();
|
||||
}
|
||||
println!(
|
||||
"Bump regression: 26 stock clips, 48 simplexes, {} parameter probes, logged graph behaviors 3623/3624 in both stances",
|
||||
points.len()
|
||||
);
|
||||
}
|
||||
@@ -45,6 +45,7 @@ fn stock_motion_host_loads_graph_settings_and_authored_riding_tree() {
|
||||
.iter()
|
||||
.map(count)
|
||||
.fold((0, 1), |(ac, ap), (bc, bp)| (ac + bc, ap + bp)),
|
||||
PlaybackTree::BlendSpace(tree) => tree.children.iter().map(count).fold((0,0), |(ac,ap),(bc,bp)| (ac+bc,ap+bp)),
|
||||
PlaybackTree::Transition(_) => {
|
||||
panic!("Authored bank unexpectedly contains a runtime transition")
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ pub(crate) fn advance(
|
||||
let deck = physics.board.part_transforms()[BodyId::Deck.index()];
|
||||
let (fakie, mirrored) = skater.animation.stance();
|
||||
let feedback = skater.physical_feedback;
|
||||
skater.animation.motion.bump_acceleration = Some(feedback.ground_acceleration);
|
||||
skater.animation.motion.wipeout_physical = Some(crate::graph_host::motion_wipeout::Physical {
|
||||
over_599: physical.skeleton.over_599 != 0,
|
||||
collision_time_144: physical.animation.collision_time_144,
|
||||
|
||||
Reference in new issue
Block a user