use crate::math::Mat4;
use crate::mesh::{Animation, Local, Placed, Rig};
pub(crate) const BLENDED: usize = 3;
const NEARER: f32 = 0.5;
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct Posing {
pub(crate) from: Sampled,
pub(crate) over: [Option<Faded>; BLENDED],
}
impl Posing {
pub(crate) fn clip(clip: u32, at: f32) -> Self {
Self {
from: Sampled { clip, at },
over: [None; BLENDED],
}
}
pub(crate) fn blended(mut self, sampled: Sampled, weight: f32) -> Self {
let faded = Faded {
sampled,
weight: weight.clamp(0.0, 1.0),
};
match self.over.iter().position(Option::is_none) {
Some(free) => self.over[free] = Some(faded),
None => {
self = self.collapsed();
self.over[BLENDED - 1] = Some(faded);
}
}
self
}
fn collapsed(mut self) -> Self {
if let Some(oldest) = self.over[0].filter(|oldest| oldest.weight >= NEARER) {
self.from = oldest.sampled;
}
self.over.rotate_left(1);
self.over[BLENDED - 1] = None;
self
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct Sampled {
pub(crate) clip: u32,
pub(crate) at: f32,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct Faded {
pub(crate) sampled: Sampled,
pub(crate) weight: f32,
}
impl Sampled {
fn of(self, clips: &[Animation]) -> Option<&Animation> {
clips.get(self.clip as usize)
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub(crate) struct Pose {
locals: Vec<Local>,
}
impl Pose {
fn rest(&mut self, rig: &Rig) {
self.locals.clear();
self.locals
.extend(rig.joints().iter().map(|joint| joint.rest));
}
fn sample(&mut self, rig: &Rig, animation: Option<&Animation>, at: f32) {
self.rest(rig);
let Some(animation) = animation else {
return;
};
for track in animation.tracks() {
let Some(local) = self.locals.get_mut(track.joint as usize) else {
continue;
};
*local = track.moves.moved(*local, at);
}
}
fn blend(&mut self, other: &Self, weight: f32) {
let weight = weight.clamp(0.0, 1.0);
for (local, &blended) in self.locals.iter_mut().zip(&other.locals) {
*local = local.mixed(blended, weight);
}
}
fn compose(&self, rig: &Rig, into: &mut Vec<Mat4>) {
let start = into.len();
for (at, joint) in rig.joints().iter().enumerate() {
let local = self.locals.get(at).copied().unwrap_or(joint.rest);
let above = match joint.placed {
Placed::Under(parent) => into
.get(start + parent as usize)
.copied()
.unwrap_or(Mat4::IDENTITY),
Placed::Within(above) => above,
};
into.push(above * local.matrix());
}
for (matrix, joint) in into[start..].iter_mut().zip(rig.joints()) {
*matrix *= joint.bind;
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct Skinned(pub(crate) u32);
#[derive(Default)]
pub(crate) struct Palette {
matrices: Vec<Mat4>,
taken: Vec<Taken>,
posed: Pose,
blended: Pose,
}
impl Palette {
pub(crate) fn clear(&mut self) {
self.matrices.clear();
self.taken.clear();
}
pub(crate) fn take(
&mut self,
mesh: Skinned,
rig: &Rig,
clips: &[Animation],
posing: Option<Posing>,
) -> u32 {
if !rig.skins() {
return 0;
}
let matching = self
.taken
.iter()
.rev()
.find(|taken| taken.mesh == mesh && taken.posing == posing);
if let Some(taken) = matching {
return taken.at;
}
let at = self.composed(rig, clips, posing);
self.taken.push(Taken { mesh, posing, at });
at
}
pub(crate) fn composed(
&mut self,
rig: &Rig,
clips: &[Animation],
posing: Option<Posing>,
) -> u32 {
if !rig.skins() {
return 0;
}
let Self {
matrices,
posed,
blended,
..
} = self;
let at = matrices.len() as u32;
match posing {
None => posed.rest(rig),
Some(posing) => {
posed.sample(rig, posing.from.of(clips), posing.from.at);
for faded in posing.over.iter().flatten() {
blended.sample(rig, faded.sampled.of(clips), faded.sampled.at);
posed.blend(blended, faded.weight);
}
}
}
posed.compose(rig, matrices);
at
}
pub(crate) fn matrices(&self) -> &[Mat4] {
&self.matrices
}
}
struct Taken {
mesh: Skinned,
posing: Option<Posing>,
at: u32,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::assets::{Assets, RIG, SCALED, file};
use crate::math::{Quat, Vec3};
use crate::mesh::{Clip, Geometry, Joint, Keys, Moves, NoParts, Track, Weighted};
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
enum Paces {
Idle,
Walk,
}
impl Clip for Paces {
fn from_name(name: &str) -> Option<Self> {
match name {
"idle" => Some(Self::Idle),
"walk" => Some(Self::Walk),
_ => None,
}
}
fn all() -> Vec<Self> {
vec![Self::Idle, Self::Walk]
}
fn index(&self) -> u32 {
self.clone() as u32
}
}
fn rig_of(source: &[u8]) -> Geometry {
let assets = Assets::load([file("rig.glb", source)]).expect("the fixture decodes");
assets
.model::<NoParts, Paces>("Rig")
.erased()
.expect("built whole")
}
fn joint(placed: Placed, position: Vec3, above: Mat4) -> Joint {
let rest = Local::new(position, Quat::IDENTITY, Vec3::ONE);
Joint {
placed,
rest,
bind: (above * rest.matrix()).inverse(),
}
}
fn stack() -> Rig {
let root = joint(Placed::Within(Mat4::IDENTITY), Vec3::Y, Mat4::IDENTITY);
Rig::new(
vec![root, joint(Placed::Under(0), Vec3::Y, root.rest.matrix())],
vec![Weighted::whole(0), Weighted::whole(1)],
)
}
fn clip(joint: u32, moves: Moves) -> Vec<Animation> {
vec![Animation::new(vec![Track { joint, moves }])]
}
fn palette(rig: &Rig, clips: &[Animation], posing: Option<Posing>) -> Vec<Mat4> {
let mut palette = Palette::default();
palette.composed(rig, clips, posing);
palette.matrices().to_vec()
}
fn moved(moves: Moves, at: f32) -> Local {
let rig = Rig::new(
vec![joint(
Placed::Within(Mat4::IDENTITY),
Vec3::ZERO,
Mat4::IDENTITY,
)],
vec![Weighted::whole(0)],
);
let clips = clip(0, moves);
let mut pose = Pose::default();
pose.sample(&rig, clips.first(), at);
pose.locals[0]
}
fn position(moves: Moves, at: f32) -> Vec3 {
moved(moves, at).position
}
fn stepping() -> Vec<(f32, Vec3)> {
vec![
(1.0, Vec3::ZERO),
(2.0, Vec3::X * 10.0),
(4.0, Vec3::X * 20.0),
]
}
#[test]
fn a_channel_reads_its_keys_and_mixes_between_them() {
let moves = Moves::Position(Keys::Linear(stepping()));
assert_eq!(position(moves.clone(), 1.0), Vec3::ZERO, "at a key");
assert_eq!(position(moves.clone(), 2.0), Vec3::X * 10.0);
assert_eq!(
position(moves.clone(), 1.5),
Vec3::X * 5.0,
"halfway between two keys"
);
assert_eq!(
position(moves, 3.0),
Vec3::X * 15.0,
"and halfway along an interval twice as long"
);
}
#[test]
fn a_time_either_side_of_the_ends_reads_the_nearest_end() {
let moves = Moves::Position(Keys::Linear(stepping()));
assert_eq!(position(moves.clone(), 0.0), Vec3::ZERO);
assert_eq!(position(moves.clone(), -100.0), Vec3::ZERO);
assert_eq!(position(moves.clone(), 4.5), Vec3::X * 20.0);
assert_eq!(position(moves, 1e9), Vec3::X * 20.0);
}
#[test]
fn a_step_channel_holds_the_earlier_key_until_the_next() {
let moves = Moves::Position(Keys::Step(stepping()));
assert_eq!(position(moves.clone(), 1.0), Vec3::ZERO);
assert_eq!(position(moves.clone(), 1.99), Vec3::ZERO, "held, not mixed");
assert_eq!(position(moves.clone(), 2.0), Vec3::X * 10.0);
assert_eq!(position(moves, 3.9), Vec3::X * 10.0);
}
#[test]
fn a_turn_takes_the_shorter_way_round_whichever_way_a_key_spells_it() {
let quarter = Quat::from_rotation_y(core::f32::consts::FRAC_PI_2);
let turning = |later: Quat| {
let moves = Moves::Turn(Keys::Linear(vec![(0.0, Quat::IDENTITY), (1.0, later)]));
moved(moves, 0.5).turn
};
let eighth = Quat::from_rotation_y(core::f32::consts::FRAC_PI_4);
assert!(turning(quarter).abs_diff_eq(eighth, 1e-5));
assert!(
turning(-quarter).abs_diff_eq(eighth, 1e-5)
|| turning(-quarter).abs_diff_eq(-eighth, 1e-5),
"the same turn spelled the other way round mixes the same way"
);
let halfway = turning(-quarter);
assert!(
(halfway.to_scaled_axis().length() - eighth.to_scaled_axis().length()).abs() < 1e-5,
"{halfway} turned further than half of a quarter"
);
}
#[test]
fn a_cubic_key_between_two_keys_reads_the_spline_and_a_key_reads_the_key() {
let keys = vec![
(0.0, [Vec3::ZERO, Vec3::ZERO, Vec3::X * 3.0]),
(1.0, [Vec3::Y * 3.0, Vec3::X, Vec3::ZERO]),
];
let moves = Moves::Position(Keys::Cubic(keys));
assert_eq!(position(moves.clone(), 0.0), Vec3::ZERO, "at the first key");
assert_eq!(position(moves.clone(), 1.0), Vec3::X, "and at the last");
let middle = position(moves, 0.5);
assert!(
middle.abs_diff_eq(Vec3::new(0.875, -0.375, 0.0), 1e-6),
"{middle} is not where the spline lies halfway across"
);
}
#[test]
fn two_keys_at_one_time_read_the_later_of_the_two() {
let keys = vec![
(0.0, Vec3::ZERO),
(1.0, Vec3::X),
(1.0, Vec3::Y),
(2.0, Vec3::Y * 2.0),
];
let stepped = Moves::Position(Keys::Step(keys.clone()));
let mixed = Moves::Position(Keys::Linear(keys));
assert_eq!(position(stepped, 1.0), Vec3::Y);
assert_eq!(position(mixed.clone(), 1.0), Vec3::Y);
assert_eq!(
position(mixed, 1.5),
Vec3::Y * 1.5,
"and the interval past them starts at the later key"
);
}
#[test]
fn a_path_no_track_moves_holds_the_rest() {
let turned = Moves::Turn(Keys::Linear(vec![(
0.0,
Quat::from_rotation_x(core::f32::consts::FRAC_PI_2),
)]));
let rig = stack();
let clips = clip(1, turned);
let mut pose = Pose::default();
pose.sample(&rig, clips.first(), 0.0);
assert_eq!(
pose.locals[0],
rig.joints()[0].rest,
"the joint no track names is left at rest"
);
let moved = pose.locals[1];
assert_eq!(
(moved.position, moved.scale),
(rig.joints()[1].rest.position, rig.joints()[1].rest.scale),
"and the paths of the joint it does name that it never moves"
);
assert_ne!(moved.turn, rig.joints()[1].rest.turn);
}
#[test]
fn a_joint_is_placed_within_its_parent_and_never_its_parent_within_it() {
let rig = stack();
let turn = Moves::Turn(Keys::Step(vec![(
0.0,
Quat::from_rotation_z(core::f32::consts::FRAC_PI_2),
)]));
let tip = Vec3::Y * 3.0;
let turned = |joint| {
let posed = palette(&rig, &clip(joint, turn.clone()), Some(Posing::clip(0, 0.0)));
[posed[0], posed[1]].map(|matrix| matrix.transform_point3(tip))
};
let [by_root, under_root] = turned(0);
assert!(
by_root.abs_diff_eq(Vec3::new(-2.0, 1.0, 0.0), 1e-5),
"{by_root} is not where the turned root takes a corner two meters above it"
);
assert!(
under_root.abs_diff_eq(by_root, 1e-5),
"{under_root}, the joint under it, carries that corner the same way"
);
let [by_parent, moved] = turned(1);
assert_eq!(by_parent, tip, "a turn under a joint leaves that joint be");
assert!(
moved.abs_diff_eq(Vec3::new(-1.0, 2.0, 0.0), 1e-5),
"{moved} is not where the turned joint takes a corner a meter above it"
);
}
#[test]
fn a_rig_at_rest_composes_to_the_identity_palette_a_model_is_drawn_by() {
for source in [RIG, SCALED] {
let rig = rig_of(source);
let at_rest = palette(rig.rig(), rig.clips(), None);
assert_eq!(at_rest.len(), 3, "one matrix per joint");
for (at, matrix) in at_rest.iter().enumerate() {
assert!(
matrix.abs_diff_eq(Mat4::IDENTITY, 1e-5),
"joint {at} composes to {matrix} where its bind is the inverse of where it \
rests in the model"
);
}
}
}
#[test]
fn the_root_node_a_scaled_model_stands_in_reaches_its_corners_and_not_its_palette() {
let scaled = rig_of(SCALED);
let Placed::Within(above) = scaled.rig().joints()[0].placed else {
panic!("the root joint hangs under no joint");
};
let placement = Mat4::from_scale_rotation_translation(
Vec3::splat(0.5),
Quat::from_rotation_y(core::f32::consts::FRAC_PI_6),
Vec3::new(1.0, 0.0, -2.0),
);
assert!(
above.abs_diff_eq(placement, 1e-6),
"the root node's own transform stands above the joints"
);
let plain = rig_of(RIG);
assert_ne!(
scaled.vertices().len(),
0,
"and the corners the source states already carry it"
);
assert_ne!(
scaled.vertices()[0].position,
plain.vertices()[0].position,
"so a scaled model's corners lie where the scale leaves them"
);
}
fn sampled(clip: u32, at: f32) -> Sampled {
Sampled { clip, at }
}
#[test]
fn a_blend_at_either_end_is_the_pose_at_that_end_and_a_pose_blends_to_itself() {
let rig = rig_of(RIG);
let ends = |weight| {
palette(
rig.rig(),
rig.clips(),
Some(Posing::clip(0, 0.4).blended(sampled(1, 0.4), weight)),
)
};
let alone = |clip| palette(rig.rig(), rig.clips(), Some(Posing::clip(clip, 0.4)));
assert_eq!(ends(0.0), alone(0));
assert_eq!(ends(1.0), alone(1));
assert_ne!(alone(0), alone(1), "the two clips hold poses of their own");
let itself = palette(
rig.rig(),
rig.clips(),
Some(Posing::clip(1, 0.4).blended(sampled(1, 0.4), 0.5)),
);
assert_eq!(itself, alone(1));
}
#[test]
fn a_pose_reads_four_clips_however_many_are_blended_over_it() {
let blended = (0..8).fold(Posing::clip(0, 0.0), |posing, over| {
posing.blended(sampled(1, over as f32), 0.9)
});
assert_eq!(
blended.over.iter().flatten().count(),
BLENDED,
"three over the one it starts at, and no more"
);
assert_eq!(
blended.from,
sampled(1, 4.0),
"and the collapsed pairs kept the clip each lay nearer"
);
let under = (0..8).fold(Posing::clip(0, 0.0), |posing, over| {
posing.blended(sampled(1, over as f32), 0.1)
});
assert_eq!(
under.from,
sampled(0, 0.0),
"a pair the pose lies nearer the first of keeps that one"
);
}
#[test]
fn a_mesh_with_no_joints_takes_no_matrices() {
let mut palette = Palette::default();
assert_eq!(palette.take(Skinned(0), &Rig::default(), &[], None), 0);
assert!(palette.matrices().is_empty());
}
fn taking(
palette: &mut Palette,
model: &Geometry,
mesh: Skinned,
posing: Option<Posing>,
) -> u32 {
palette.take(mesh, model.rig(), model.clips(), posing)
}
#[test]
fn draws_of_one_mesh_in_one_pose_take_one_run_of_the_palette() {
let model = rig_of(RIG);
let joints = model.rig().joints().len() as u32;
let mut palette = Palette::default();
for _ in 0..90 {
assert_eq!(
taking(&mut palette, &model, Skinned(0), None),
0,
"every draw reads the first run"
);
}
assert_eq!(
palette.matrices().len() as u32,
joints,
"ninety draws of one mesh at rest take one run of it"
);
let walking = Posing::clip(1, 0.4);
assert_eq!(
taking(&mut palette, &model, Skinned(1), None),
joints,
"a second mesh takes a run of its own"
);
assert_eq!(
taking(&mut palette, &model, Skinned(0), Some(walking)),
2 * joints,
"and so does a pose of the first"
);
assert_eq!(
taking(&mut palette, &model, Skinned(0), Some(Posing::clip(1, 0.4))),
2 * joints,
"a draw in a pose equal to one already taken reads that run"
);
assert_eq!(
taking(&mut palette, &model, Skinned(0), None),
0,
"and the rest run stands where it was"
);
assert_eq!(palette.matrices().len() as u32, 3 * joints);
palette.clear();
assert_eq!(
taking(&mut palette, &model, Skinned(0), Some(walking)),
0,
"a frame takes afresh"
);
}
}