use proof_engine::anim::particle_skin::ParticleSkin;
use proof_engine::anim::skeleton::{BoneId, Pose, Skeleton, SkeletonBuilder, Transform3D};
use glam::{Quat, Vec3};
fn arm() -> Skeleton {
SkeletonBuilder::new()
.add_bone("upper", None, Transform3D::identity())
.add_bone(
"fore",
Some("upper"),
Transform3D::new(Vec3::X, Quat::IDENTITY, Vec3::ONE),
)
.add_bone(
"hand",
Some("fore"),
Transform3D::new(Vec3::X, Quat::IDENTITY, Vec3::ONE),
)
.build()
}
#[test]
fn a_rest_pose_moves_nothing() {
let s = arm();
let pts: Vec<Vec3> = (0..40)
.map(|i| Vec3::new(i as f32 * 0.05, (i % 5) as f32 * 0.02, 0.0))
.collect();
let skin = ParticleSkin::bind_nearest(&pts, &s, 0.3);
let mut out = Vec::new();
skin.apply(&s, &s.rest_pose(), &pts, &mut out);
assert_eq!(out.len(), pts.len());
for (a, b) in pts.iter().zip(out.iter()) {
assert!(
(*a - *b).length() < 1e-4,
"the rest pose moved a particle: {a} -> {b}"
);
}
}
#[test]
fn bending_a_joint_carries_the_matter_past_it() {
let s = arm();
let upper = Vec3::new(0.5, 0.0, 0.0);
let tip = Vec3::new(1.9, 0.0, 0.0);
let pts = vec![upper, tip];
let skin = ParticleSkin::bind_nearest(&pts, &s, 0.15);
let mut pose = s.rest_pose();
let mut fore = pose.get(BoneId(1)).unwrap();
fore.rotation = Quat::from_rotation_z(std::f32::consts::FRAC_PI_2);
pose.set(BoneId(1), fore);
let mut out = Vec::new();
skin.apply(&s, &pose, &pts, &mut out);
assert!(
(out[0] - upper).length() < 0.05,
"the upper arm moved when the elbow bent: {}",
out[0]
);
assert!(
(out[1] - tip).length() > 0.5,
"the elbow bent and the hand stayed put: {}",
out[1]
);
assert!(out[1].y > 0.5, "the hand bent the wrong way: {}", out[1]);
}
#[test]
fn a_particle_in_the_joint_belongs_to_both_bones() {
let s = arm();
let elbow = vec![Vec3::new(1.0, 0.0, 0.0)];
let skin = ParticleSkin::bind_nearest(&elbow, &s, 0.5);
let b = skin.bind_of(0).expect("bound");
assert!(
b.weights[1] > 0.15,
"a particle sitting in the joint is rigid to one bone: {:?}",
b.weights
);
assert!(
(b.weights[0] + b.weights[1] - 1.0).abs() < 1e-4,
"weights do not sum to one: {:?}",
b.weights
);
}
#[test]
fn a_falloff_of_nothing_binds_rigidly() {
let s = arm();
let pts = vec![Vec3::new(1.0, 0.0, 0.0)];
let skin = ParticleSkin::bind_nearest(&pts, &s, 0.0);
assert_eq!(skin.bind_of(0).unwrap().weights, [1.0, 0.0]);
}
#[test]
fn every_particle_ends_up_bound_to_something() {
let s = arm();
let stray = vec![Vec3::new(0.5, 9.0, -4.0)];
let skin = ParticleSkin::bind_nearest(&stray, &s, 0.3);
let b = skin.bind_of(0).expect("bound");
assert!(b.weights[0] > 0.0);
assert!((b.bones[0] as usize) < s.len());
}
#[test]
fn an_empty_skeleton_is_survivable() {
let s = Skeleton::new();
let pts = vec![Vec3::X, Vec3::Y];
let skin = ParticleSkin::bind_nearest(&pts, &s, 0.2);
let mut out = Vec::new();
skin.apply(&s, &Pose::new(0), &pts, &mut out);
assert_eq!(out, pts, "an empty skeleton should leave the cloud alone");
}
#[test]
fn a_cloud_that_grew_since_it_was_bound_does_not_panic() {
let s = arm();
let pts = vec![Vec3::new(0.5, 0.0, 0.0)];
let skin = ParticleSkin::bind_nearest(&pts, &s, 0.2);
let grown = vec![Vec3::new(0.5, 0.0, 0.0), Vec3::new(1.5, 0.0, 0.0)];
let mut out = Vec::new();
skin.apply(&s, &s.rest_pose(), &grown, &mut out);
assert_eq!(out.len(), 2);
assert!(out[1].is_finite());
}
#[test]
fn skinning_never_produces_a_broken_number() {
let s = arm();
let pts: Vec<Vec3> = (0..64)
.map(|i| Vec3::new(i as f32 * 0.03, (i as f32 * 0.7).sin() * 0.2, 0.0))
.collect();
let skin = ParticleSkin::bind_nearest(&pts, &s, 0.25);
let mut pose = s.rest_pose();
for (i, id) in [BoneId(0), BoneId(1), BoneId(2)].iter().enumerate() {
let mut t = pose.get(*id).unwrap();
t.rotation = Quat::from_rotation_z(i as f32 * 1.7 - 2.0);
pose.set(*id, t);
}
let mut out = Vec::new();
skin.apply(&s, &pose, &pts, &mut out);
for p in &out {
assert!(p.is_finite(), "skinning produced {p}");
}
}
#[test]
fn normals_turn_with_the_bone_they_are_bound_to() {
let s = arm();
let pts = vec![Vec3::new(1.6, 0.0, 0.0)];
let normals = vec![Vec3::Y];
let skin = ParticleSkin::bind_nearest(&pts, &s, 0.0);
let mut pose = s.rest_pose();
let mut fore = pose.get(BoneId(1)).unwrap();
fore.rotation = Quat::from_rotation_z(std::f32::consts::FRAC_PI_2);
pose.set(BoneId(1), fore);
let mut out = Vec::new();
skin.apply_normals(&s, &pose, &normals, &mut out);
assert_eq!(out.len(), 1);
assert!((out[0].length() - 1.0).abs() < 1e-4, "a normal lost its length");
assert!(out[0].x < -0.9, "the normal did not follow the bone: {}", out[0]);
}
#[test]
fn a_rest_pose_leaves_normals_alone() {
let s = arm();
let pts = vec![Vec3::new(0.5, 0.0, 0.0), Vec3::new(1.5, 0.0, 0.0)];
let normals = vec![Vec3::Y, Vec3::Z];
let skin = ParticleSkin::bind_nearest(&pts, &s, 0.2);
let mut out = Vec::new();
skin.apply_normals(&s, &s.rest_pose(), &normals, &mut out);
for (a, b) in normals.iter().zip(out.iter()) {
assert!((*a - *b).length() < 1e-4, "the rest pose turned a normal");
}
}
#[test]
fn a_group_keeps_a_point_off_the_bones_it_must_not_use() {
let s = arm();
let blade = vec![Vec3::new(0.4, 0.05, 0.0)];
let loose = ParticleSkin::bind_nearest(&blade, &s, 0.0);
assert_eq!(
loose.bind_of(0).unwrap().dominant(),
BoneId(0),
"without a group it binds to whatever is nearest"
);
let held = ParticleSkin::bind_grouped(&blade, &s, 0.0, &[0], &[vec![BoneId(2)]]);
assert_eq!(held.bind_of(0).unwrap().dominant(), BoneId(2));
let mut pose = s.rest_pose();
let mut fore = pose.get(BoneId(1)).unwrap();
fore.rotation = Quat::from_rotation_z(1.2);
pose.set(BoneId(1), fore);
let mut out = Vec::new();
held.apply(&s, &pose, &blade, &mut out);
assert!(
(out[0] - blade[0]).length() > 0.3,
"the held point did not follow the arm: {}",
out[0]
);
}
#[test]
fn an_unclassified_point_still_binds_to_something() {
let s = arm();
let pts = vec![Vec3::new(0.5, 0.0, 0.0), Vec3::new(1.5, 0.0, 0.0)];
let skin = ParticleSkin::bind_grouped(&pts, &s, 0.2, &[0, 9], &[vec![BoneId(0)]]);
assert_eq!(skin.bind_of(0).unwrap().dominant(), BoneId(0));
let b = skin.bind_of(1).unwrap();
assert!(b.weights[0] > 0.0, "an unclassified point was left unbound");
}
#[test]
fn an_empty_group_means_the_whole_skeleton() {
let s = arm();
let pts = vec![Vec3::new(1.9, 0.0, 0.0), Vec3::new(2.3, 0.0, 0.0)];
let skin = ParticleSkin::bind_grouped(&pts, &s, 0.0, &[0, 0], &[Vec::new()]);
assert_eq!(skin.bind_of(0).unwrap().dominant(), BoneId(1));
assert_eq!(skin.bind_of(1).unwrap().dominant(), BoneId(2));
}