#![allow(clippy::disallowed_methods)]
#![cfg(feature = "std")]
use alice_physics::collider::Contact;
use alice_physics::math::{Fix128, QuatFix, Vec3Fix};
use alice_physics::solver::{ContactModifier, PhysicsConfig, PhysicsWorld, RigidBody};
use alice_physics::{CollisionFilter, ContactEventType, SleepConfig};
use std::panic::{catch_unwind, AssertUnwindSafe};
fn fx(v: f64) -> Fix128 {
Fix128::from_f64(v)
}
fn v3(x: f64, y: f64, z: f64) -> Vec3Fix {
Vec3Fix::new(fx(x), fx(y), fx(z))
}
fn dt() -> Fix128 {
Fix128::from_ratio(1, 64)
}
fn weightless() -> PhysicsConfig {
PhysicsConfig {
gravity: Vec3Fix::ZERO,
..PhysicsConfig::default()
}
}
fn close(got: Fix128, want: f64, tol: f64, what: &str) {
let g = got.to_f64();
assert!(
(g - want).abs() <= tol,
"{what}: got {g}, want {want} (tol {tol})"
);
}
#[test]
fn new_body_has_inverse_mass_and_unit_sphere_inverse_inertia() {
let b = RigidBody::new(v3(1.0, 2.0, 3.0), fx(4.0));
assert_eq!(b.inv_mass, Fix128::from_ratio(1, 4));
for (axis, v) in [
("x", b.inv_inertia.x),
("y", b.inv_inertia.y),
("z", b.inv_inertia.z),
] {
close(v, 0.625, 1e-12, &format!("inv_inertia.{axis}"));
}
assert_eq!(b.position, v3(1.0, 2.0, 3.0));
assert_eq!(b.prev_position, b.position);
assert_eq!(b.rotation, QuatFix::IDENTITY);
assert_eq!(b.velocity, Vec3Fix::ZERO);
assert!(b.is_dynamic() && !b.is_kinematic() && !b.is_static());
assert_eq!(b.restitution, Fix128::from_ratio(3, 10));
assert_eq!(b.friction, Fix128::from_ratio(5, 10));
assert_eq!(b.gravity_scale, Fix128::ONE);
assert_eq!(b.linear_damping, Fix128::ONE);
assert_eq!(b.angular_damping, Fix128::ONE);
}
#[test]
fn zero_mass_body_has_zero_inverses() {
let b = RigidBody::new(Vec3Fix::ZERO, Fix128::ZERO);
assert_eq!(b.inv_mass, Fix128::ZERO);
assert_eq!(b.inv_inertia, Vec3Fix::ZERO);
assert!(b.is_static());
}
#[test]
fn mass_round_trips_through_the_inverse() {
for m in [1.0, 2.0, 3.0, 7.0, 0.1, 1000.0] {
let b = RigidBody::new(Vec3Fix::ZERO, fx(m));
close(b.mass(), m, m * 1e-12, "mass()");
}
}
#[test]
fn mass_of_a_static_body_is_the_zero_sentinel() {
assert_eq!(RigidBody::new_static(Vec3Fix::ZERO).mass(), Fix128::ZERO);
assert_eq!(RigidBody::new_kinematic(Vec3Fix::ZERO).mass(), Fix128::ZERO);
}
#[test]
fn apply_impulse_changes_velocity_by_j_over_m() {
let mut b = RigidBody::new(Vec3Fix::ZERO, fx(4.0));
b.set_velocity(v3(1.0, 1.0, 1.0));
b.apply_impulse(v3(8.0, -4.0, 2.0));
assert_eq!(b.velocity, v3(3.0, 0.0, 1.5));
assert_eq!(
b.angular_velocity,
Vec3Fix::ZERO,
"centre-of-mass impulse does not spin"
);
}
#[test]
fn impulses_do_not_move_immovable_bodies() {
for mut b in [
RigidBody::new_static(Vec3Fix::ZERO),
RigidBody::new_kinematic(Vec3Fix::ZERO),
RigidBody::new_sensor(Vec3Fix::ZERO),
] {
b.apply_impulse(v3(5.0, 5.0, 5.0));
b.apply_impulse_at(v3(5.0, 5.0, 5.0), v3(0.0, 1.0, 0.0));
assert_eq!(b.velocity, Vec3Fix::ZERO);
assert_eq!(b.angular_velocity, Vec3Fix::ZERO);
}
}
#[test]
fn apply_impulse_at_gives_linear_and_angular_response() {
let mut b = RigidBody::new(Vec3Fix::ZERO, fx(2.0));
b.apply_impulse_at(v3(1.0, 0.0, 0.0), v3(0.0, 1.0, 0.0));
assert_eq!(b.velocity, v3(0.5, 0.0, 0.0));
close(b.angular_velocity.x, 0.0, 1e-12, "wx");
close(b.angular_velocity.y, 0.0, 1e-12, "wy");
close(b.angular_velocity.z, -1.25, 1e-12, "wz");
}
#[test]
fn apply_impulse_through_the_centre_gives_no_spin() {
let mut b = RigidBody::new(v3(1.0, 1.0, 1.0), fx(2.0));
b.apply_impulse_at(v3(3.0, -2.0, 1.0), v3(4.0, -1.0, 2.0)); assert_eq!(b.angular_velocity, Vec3Fix::ZERO);
}
fn rotated_anisotropic_body() -> RigidBody {
let s = fx(0.5).sqrt(); let mut b = RigidBody::new(Vec3Fix::ZERO, Fix128::ONE);
b.inv_inertia = v3(1.0, 2.0, 4.0);
b.set_rotation(QuatFix::new(Fix128::ZERO, Fix128::ZERO, s, s));
b
}
#[test]
fn add_torque_uses_the_body_frame_inertia_of_a_rotated_body() {
let mut b = rotated_anisotropic_body();
b.add_torque(v3(1.0, 0.0, 0.0), Fix128::ONE);
close(b.angular_velocity.x, 2.0, 1e-9, "wx");
close(b.angular_velocity.y, 0.0, 1e-9, "wy");
close(b.angular_velocity.z, 0.0, 1e-9, "wz");
}
#[test]
fn apply_impulse_at_uses_the_body_frame_inertia_of_a_rotated_body() {
let mut b = rotated_anisotropic_body();
b.apply_impulse_at(v3(0.0, 0.0, 1.0), v3(0.0, 1.0, 0.0));
close(b.angular_velocity.x, 2.0, 1e-9, "wx");
}
#[test]
fn set_position_teleports_and_resets_prev_position() {
let mut b = RigidBody::new(v3(1.0, 2.0, 3.0), Fix128::ONE);
b.set_position(v3(9.0, 8.0, 7.0));
assert_eq!(b.position, v3(9.0, 8.0, 7.0));
assert_eq!(b.prev_position, b.position);
}
#[test]
fn kinematic_body_reaches_its_target_in_one_step() {
let mut w = PhysicsWorld::new(PhysicsConfig::default());
let k = w.add_body(RigidBody::new_kinematic(Vec3Fix::ZERO));
w.get_body_mut(k)
.unwrap()
.set_kinematic_target(v3(1.0, 2.0, 3.0), QuatFix::IDENTITY);
w.step(dt());
assert_eq!(w.get_body(k).unwrap().position, v3(1.0, 2.0, 3.0));
}
#[test]
fn kinematic_velocity_after_a_step_is_the_displacement_over_dt() {
let mut w = PhysicsWorld::new(weightless());
let k = w.add_body(RigidBody::new_kinematic(Vec3Fix::ZERO));
w.get_body_mut(k)
.unwrap()
.set_kinematic_target(v3(1.0, 0.0, 0.0), QuatFix::IDENTITY);
w.step(dt());
close(
w.get_body(k).unwrap().velocity.x,
64.0,
1e-6,
"kinematic vx",
);
}
#[test]
fn kinematic_pose_after_a_step_is_the_target_and_angular_velocity_matches() {
let mut w = PhysicsWorld::new(weightless());
let k = w.add_body(RigidBody::new_kinematic(Vec3Fix::ZERO));
let rot = QuatFix::from_axis_angle(v3(0.0, 0.0, 1.0), fx(0.5));
w.get_body_mut(k)
.unwrap()
.set_kinematic_target(v3(1.0, 0.0, 0.0), rot);
w.step(dt());
let b = w.get_body(k).unwrap();
assert_eq!(b.position, v3(1.0, 0.0, 0.0));
assert_eq!(b.rotation, rot);
close(b.angular_velocity.z, 32.0, 32.0 * 1e-2, "kinematic wz");
}
#[test]
fn observe_body_mirrors_the_body_and_none_out_of_range() {
let mut w = PhysicsWorld::new(weightless());
let a = w.add_body(RigidBody::new(v3(1.0, 2.0, 3.0), fx(2.0)));
let b = w.add_body(RigidBody::new(v3(-5.0, 0.0, 0.0), fx(1.0)));
{
let ba = w.get_body_mut(a).unwrap();
ba.set_velocity(v3(0.5, 0.0, -0.5));
ba.set_angular_velocity(v3(0.0, 0.25, 0.0));
}
let o = w.observe_body(a).unwrap();
assert_eq!(o.body_index, a);
assert_eq!(o.position, v3(1.0, 2.0, 3.0));
assert_eq!(o.velocity, v3(0.5, 0.0, -0.5));
assert_eq!(o.angular_velocity, v3(0.0, 0.25, 0.0));
assert_eq!(o.rotation, QuatFix::IDENTITY);
assert!(!o.sleeping && !o.in_contact);
assert!(w.observe_body(2).is_none());
assert!(w.observe_body(usize::MAX).is_none());
let all = w.observe_bodies();
assert_eq!(all.len(), 2);
assert_eq!(all[0], o);
assert_eq!(all[1].body_index, b);
assert_eq!(all[1].position, v3(-5.0, 0.0, 0.0));
assert!(PhysicsWorld::new(weightless()).observe_bodies().is_empty());
}
#[test]
fn observe_body_reports_sleeping() {
let mut w = PhysicsWorld::new(weightless());
w.set_sleep_config(SleepConfig {
frames_to_sleep: 3,
..SleepConfig::default()
});
let a = w.add_body(RigidBody::new(Vec3Fix::ZERO, Fix128::ONE));
assert!(!w.observe_body(a).unwrap().sleeping);
for _ in 0..5 {
w.step(dt());
}
assert!(w.is_sleeping(a));
assert!(w.observe_body(a).unwrap().sleeping);
}
fn touching_pair() -> (PhysicsWorld, usize, usize) {
let mut w = PhysicsWorld::new(weightless());
let a = w.add_body_with_radius(RigidBody::new(v3(0.0, 0.0, 0.0), Fix128::ONE), Fix128::ONE);
let b = w.add_body_with_radius(RigidBody::new_static(v3(1.5, 0.0, 0.0)), Fix128::ONE);
(w, a, b)
}
#[test]
fn in_contact_is_true_while_a_contact_begins() {
let (mut w, a, b) = touching_pair();
w.step(dt());
assert!(
w.contact_events()
.iter()
.any(|e| e.event_type == ContactEventType::Begin),
"scene must produce a Begin event"
);
assert!(w.observe_body(a).unwrap().in_contact);
assert!(w.observe_body(b).unwrap().in_contact);
}
#[test]
#[ignore = "known defect: AUD-A-S1W2-003: observe_body.in_contact is true on the frame of an End event (any contact event counts), doc says Begin or Persist only"]
fn in_contact_is_false_on_the_frame_the_contact_ends() {
let (mut w, a, _b) = touching_pair();
w.step(dt());
w.get_body_mut(a).unwrap().set_position(v3(-10.0, 0.0, 0.0));
w.step(dt());
let evs = w.contact_events();
assert!(
evs.iter().any(|e| e.event_type == ContactEventType::End),
"scene must produce an End event, got {} events",
evs.len()
);
assert!(
evs.iter().all(|e| e.event_type == ContactEventType::End),
"only End events this frame"
);
assert!(!w.observe_body(a).unwrap().in_contact);
}
fn ray_world() -> PhysicsWorld {
PhysicsWorld::new(weightless())
}
#[test]
fn raycast_hits_the_near_surface_of_a_sphere() {
let mut w = ray_world();
let s = w.add_body_with_radius(RigidBody::new(v3(5.0, 0.0, 0.0), Fix128::ONE), Fix128::ONE);
let (i, t) = w
.raycast(Vec3Fix::ZERO, v3(1.0, 0.0, 0.0), fx(100.0))
.expect("hit");
assert_eq!(i, s);
close(t, 4.0, 1e-9, "t");
}
#[test]
fn raycast_distance_is_in_world_units_for_a_long_direction() {
let mut w = ray_world();
w.add_body_with_radius(RigidBody::new(v3(0.0, 0.0, 5.0), Fix128::ONE), Fix128::ONE);
let (_, t) = w
.raycast(Vec3Fix::ZERO, v3(0.0, 0.0, 3.0), fx(100.0))
.expect("hit");
close(t, 4.0, 1e-9, "t");
}
#[test]
fn raycast_respects_max_distance() {
let mut w = ray_world();
w.add_body_with_radius(RigidBody::new(v3(5.0, 0.0, 0.0), Fix128::ONE), Fix128::ONE);
assert!(w
.raycast(Vec3Fix::ZERO, v3(1.0, 0.0, 0.0), fx(3.9))
.is_none());
assert!(w
.raycast(Vec3Fix::ZERO, v3(1.0, 0.0, 0.0), fx(4.1))
.is_some());
}
#[test]
fn raycast_ignores_spheres_behind_and_beside() {
let mut w = ray_world();
w.add_body_with_radius(RigidBody::new(v3(-5.0, 0.0, 0.0), Fix128::ONE), Fix128::ONE);
w.add_body_with_radius(RigidBody::new(v3(5.0, 3.0, 0.0), Fix128::ONE), Fix128::ONE);
assert!(w
.raycast(Vec3Fix::ZERO, v3(1.0, 0.0, 0.0), fx(100.0))
.is_none());
}
#[test]
fn raycast_returns_the_nearest_sphere() {
for order in [[8.0, 3.0, 12.0], [3.0, 8.0, 12.0], [12.0, 8.0, 3.0]] {
let mut w = ray_world();
let mut near = usize::MAX;
for x in order {
let i = w.add_body_with_radius(RigidBody::new(v3(x, 0.0, 0.0), Fix128::ONE), fx(0.5));
if x == 3.0 {
near = i;
}
}
let (i, t) = w
.raycast(Vec3Fix::ZERO, v3(1.0, 0.0, 0.0), fx(100.0))
.unwrap();
assert_eq!(i, near);
close(t, 2.5, 1e-9, "t");
}
}
#[test]
fn raycast_from_inside_a_sphere_returns_the_exit_distance() {
let mut w = ray_world();
w.add_body_with_radius(RigidBody::new(Vec3Fix::ZERO, Fix128::ONE), fx(2.0));
let (_, t) = w
.raycast(v3(0.5, 0.0, 0.0), v3(1.0, 0.0, 0.0), fx(100.0))
.unwrap();
close(t, 1.5, 1e-9, "t");
}
#[test]
fn raycast_degenerate_inputs_answer_none() {
let mut w = ray_world();
w.add_body(RigidBody::new(v3(5.0, 0.0, 0.0), Fix128::ONE)); assert!(w
.raycast(Vec3Fix::ZERO, v3(1.0, 0.0, 0.0), fx(100.0))
.is_none());
w.add_body_with_radius(RigidBody::new(v3(5.0, 0.0, 0.0), Fix128::ONE), Fix128::ONE);
assert!(w.raycast(Vec3Fix::ZERO, Vec3Fix::ZERO, fx(100.0)).is_none());
}
#[test]
fn raycast_diagonal_ray() {
let mut w = ray_world();
w.add_body_with_radius(RigidBody::new(v3(3.0, 4.0, 0.0), Fix128::ONE), Fix128::ONE);
let (_, t) = w
.raycast(Vec3Fix::ZERO, v3(3.0, 4.0, 0.0), fx(100.0))
.unwrap();
close(t, 4.0, 1e-9, "t");
}
#[test]
fn combined_material_out_of_range_is_the_default_pair() {
let mut w = PhysicsWorld::new(weightless());
let a = w.add_body(RigidBody::new(Vec3Fix::ZERO, Fix128::ONE));
let b = w.add_body(RigidBody::new(Vec3Fix::ZERO, Fix128::ONE));
let d = w.combined_material(a, b);
assert_eq!(w.combined_material(a, 99), d);
assert_eq!(w.combined_material(99, 100), d);
}
#[test]
#[ignore = "known defect: AUD-A-S1W2-004: remove_body does not remap the event pair history; the surviving pair re-reports Begin and an End is emitted for a body index that no longer exists"]
fn remove_body_keeps_the_contact_history_of_the_survivors() {
let mut w = PhysicsWorld::new(weightless());
let a = w.add_body_with_radius(RigidBody::new_static(v3(0.0, 0.0, 0.0)), Fix128::ONE);
let far = w.add_body_with_radius(RigidBody::new_static(v3(100.0, 0.0, 0.0)), Fix128::ONE);
let c = w.add_body_with_radius(RigidBody::new(v3(1.5, 0.0, 0.0), Fix128::ONE), Fix128::ONE);
assert_eq!((a, far, c), (0, 1, 2));
w.step(dt());
assert!(w
.contact_events()
.iter()
.any(|e| e.event_type == ContactEventType::Begin && e.body_a == 0 && e.body_b == 2));
w.remove_body(far); w.get_body_mut(1).unwrap().set_position(v3(1.5, 0.0, 0.0));
w.step(dt());
let evs: Vec<_> = w.contact_events().to_vec();
let n = w.body_count();
assert!(
evs.iter().all(|e| e.body_a < n && e.body_b < n),
"event refers to a removed body index: {evs:?}"
);
assert!(
evs.iter()
.all(|e| e.event_type == ContactEventType::Persist),
"expected only Persist for the surviving pair, got {evs:?}"
);
}
#[test]
fn zero_substeps_does_not_panic_or_move_a_body() {
let mut w = PhysicsWorld::new(PhysicsConfig {
substeps: 0,
..PhysicsConfig::default()
});
let a = w.add_body(RigidBody::new(v3(0.0, 5.0, 0.0), Fix128::ONE));
w.step(dt());
assert_eq!(w.get_body(a).unwrap().position, v3(0.0, 5.0, 0.0));
}
struct SetMaterial {
friction: Option<Fix128>,
restitution: Option<Fix128>,
}
impl ContactModifier for SetMaterial {
fn modify_contact(
&self,
_a: usize,
_b: usize,
_contact: &mut Contact,
friction: &mut Fix128,
restitution: &mut Fix128,
) -> bool {
if let Some(f) = self.friction {
*friction = f;
}
if let Some(r) = self.restitution {
*restitution = r;
}
true
}
}
fn collision(friction: f64, restitution: f64, modifier: Option<SetMaterial>) -> (Vec3Fix, Vec3Fix) {
let mut w = PhysicsWorld::new(weightless());
let a = w.add_body_with_radius(
RigidBody::new(v3(0.0, 0.0, 0.0), Fix128::ONE).with_velocity(v3(20.0, 5.0, 0.0)),
Fix128::ONE,
);
let b = w.add_body_with_radius(
RigidBody::new(v3(2.1, 0.0, 0.0), Fix128::ONE).with_velocity(v3(-20.0, 0.0, 0.0)),
Fix128::ONE,
);
let id = w
.material_table
.register(alice_physics::PhysicsMaterial::new(
0,
fx(friction),
fx(restitution),
));
w.set_body_material(a, id);
w.set_body_material(b, id);
if let Some(m) = modifier {
w.add_contact_modifier(Box::new(m));
}
w.step(dt());
(
w.get_body(a).unwrap().velocity,
w.get_body(b).unwrap().velocity,
)
}
#[test]
fn collision_scene_responds_to_the_material_restitution() {
let (a0, b0) = collision(0.5, 0.0, None);
close(a0.x, 0.0, 0.1, "a vx");
close(b0.x, 0.0, 0.1, "b vx");
let (a1, b1) = collision(0.5, 1.0, None);
assert!(
a1.x.to_f64() < -4.0 && b1.x.to_f64() > 4.0,
"rebound {a1:?} {b1:?}"
);
}
#[test]
fn collision_scene_responds_to_the_material_friction() {
let (a_free, b_free) = collision(0.0, 1.0, None);
let (a_grip, b_grip) = collision(9.0, 1.0, None);
let rel = |a: Vec3Fix, b: Vec3Fix| (a.y - b.y).to_f64().abs();
assert!(rel(a_free, b_free) > 4.0, "free {}", rel(a_free, b_free));
assert!(rel(a_grip, b_grip) < 1.0, "grip {}", rel(a_grip, b_grip));
}
#[test]
fn a_contact_modifier_can_change_the_restitution() {
let (a, b) = collision(
0.5,
0.0,
Some(SetMaterial {
friction: None,
restitution: Some(Fix128::ONE),
}),
);
assert!(
a.x.to_f64() < -4.0 && b.x.to_f64() > 4.0,
"no rebound: {a:?} {b:?}"
);
}
#[test]
fn a_contact_modifier_can_change_the_friction() {
let (a, b) = collision(
9.0,
1.0,
Some(SetMaterial {
friction: Some(Fix128::ZERO),
restitution: None,
}),
);
let rel = (a.y - b.y).to_f64().abs();
assert!(
rel > 4.0,
"tangential relative speed {rel}, friction 0 must keep it"
);
}
#[cfg(feature = "gpu-solver-bridge")]
mod bridge {
use super::*;
use alice_physics::gpu_bridge::{DiffFixture, GpuDivergence, GpuSolverBridge};
use alice_physics::solver::ContactConstraint;
use std::cell::RefCell;
#[derive(Default)]
struct Recorder {
sent: Vec<ContactConstraint>,
positions_sent: usize,
inv_masses_sent: Vec<Fix128>,
answer: RefCell<()>,
}
impl GpuSolverBridge for Recorder {
fn send_island(&mut self, _p: &[[Fix128; 3]], _v: &[[Fix128; 3]]) {}
fn dispatch_iterations(&mut self, _iters: u32, _dt: Fix128) {}
fn recv_island(&self, _p: &mut [[Fix128; 3]], _v: &mut [[Fix128; 3]]) {}
fn assert_bit_exact_vs_cpu(&self, _f: &DiffFixture) -> Result<(), GpuDivergence> {
Ok(())
}
fn send_contact_constraints(&mut self, c: &[ContactConstraint]) {
self.sent = c.to_vec();
}
fn send_body_state(&mut self, p: &[[Fix128; 3]], i: &[Fix128]) {
self.positions_sent = p.len();
self.inv_masses_sent = i.to_vec();
}
fn dispatch_contact_solve_iteration(&mut self, _w: Fix128) {
let _ = &self.answer;
}
fn recv_contact_constraints(&self, c: &mut [ContactConstraint]) {
for (i, k) in c.iter_mut().enumerate() {
k.cached_lambda = Fix128::from_int(7 + i as i64);
}
}
fn recv_body_positions(&self, p: &mut [[Fix128; 3]]) {
p[0][0] = Fix128::from_int(42);
}
}
fn contact(depth: f64) -> Contact {
Contact {
depth: fx(depth),
normal: v3(1.0, 0.0, 0.0),
point_a: Vec3Fix::ZERO,
point_b: Vec3Fix::ZERO,
}
}
fn three_body_world() -> PhysicsWorld {
let mut w = PhysicsWorld::new(weightless());
w.add_body(RigidBody::new(v3(0.0, 0.0, 0.0), Fix128::ONE));
w.add_body(RigidBody::new(v3(1.0, 0.0, 0.0), fx(2.0)));
w.add_body(RigidBody::new(v3(2.0, 0.0, 0.0), fx(4.0)));
w
}
#[test]
fn contact_solve_filters_sends_survivors_and_writes_back_by_original_slot() {
let mut w = three_body_world();
let sensor = w.add_body(RigidBody::new_sensor(v3(5.0, 0.0, 0.0)));
w.add_pre_solve_hook(Box::new(|a, b, _c| !(a == 0 && b == 1)));
for (a, b) in [(0, 1), (1, 2), (0, sensor), (0, 2)] {
w.add_contact(ContactConstraint {
body_a: a,
body_b: b,
contact: contact(0.25),
friction: fx(0.5),
restitution: fx(0.25),
cached_lambda: Fix128::ZERO,
});
}
let mut rec = Recorder::default();
w.solve_contact_constraints_with_bridge(&mut rec);
let pairs: Vec<(usize, usize)> = rec.sent.iter().map(|c| (c.body_a, c.body_b)).collect();
assert_eq!(pairs, vec![(1, 2), (0, 2)], "sent {pairs:?}");
assert_eq!(rec.positions_sent, 4);
assert_eq!(
rec.inv_masses_sent,
vec![
Fix128::ONE,
Fix128::from_ratio(1, 2),
Fix128::from_ratio(1, 4),
Fix128::ZERO
]
);
let lam = |i: usize| w.contact_constraints[i].cached_lambda;
assert_eq!(lam(0), Fix128::ZERO, "vetoed slot untouched");
assert_eq!(lam(1), Fix128::from_int(7));
assert_eq!(lam(2), Fix128::ZERO, "sensor slot untouched");
assert_eq!(lam(3), Fix128::from_int(8));
assert_eq!(w.get_body(0).unwrap().position.x, Fix128::from_int(42));
assert_eq!(w.get_body(1).unwrap().position.x, Fix128::ONE);
}
#[test]
fn contact_solve_with_no_survivor_does_not_touch_the_bridge() {
let mut w = three_body_world();
w.add_pre_solve_hook(Box::new(|_a, _b, _c| false));
w.add_contact(ContactConstraint {
body_a: 0,
body_b: 1,
contact: contact(0.25),
friction: fx(0.5),
restitution: fx(0.25),
cached_lambda: Fix128::ZERO,
});
let mut rec = Recorder::default();
w.solve_contact_constraints_with_bridge(&mut rec);
assert!(rec.sent.is_empty());
assert_eq!(rec.positions_sent, 0, "send_body_state must not be called");
assert_eq!(w.get_body(0).unwrap().position, Vec3Fix::ZERO);
}
#[derive(Default)]
struct JointReference {
joints: Vec<alice_physics::joint::Joint>,
bodies: Vec<RigidBody>,
}
impl GpuSolverBridge for JointReference {
fn send_island(&mut self, _p: &[[Fix128; 3]], _v: &[[Fix128; 3]]) {}
fn dispatch_iterations(&mut self, _iters: u32, _dt: Fix128) {}
fn recv_island(&self, _p: &mut [[Fix128; 3]], _v: &mut [[Fix128; 3]]) {}
fn assert_bit_exact_vs_cpu(&self, _f: &DiffFixture) -> Result<(), GpuDivergence> {
Ok(())
}
fn send_joints(&mut self, j: &[alice_physics::joint::Joint]) {
self.joints = j.to_vec();
}
fn send_body_state(&mut self, p: &[[Fix128; 3]], inv_masses: &[Fix128]) {
self.bodies = p
.iter()
.zip(inv_masses)
.map(|(pos, inv_m)| {
let mut b = RigidBody::new(Vec3Fix::new(pos[0], pos[1], pos[2]), Fix128::ONE);
b.inv_mass = *inv_m;
b
})
.collect();
}
fn send_body_rotations(&mut self, r: &[[Fix128; 4]]) {
for (b, q) in self.bodies.iter_mut().zip(r) {
b.rotation = QuatFix::new(q[0], q[1], q[2], q[3]);
}
}
fn dispatch_joint_solve_iteration(&mut self, dt: Fix128) {
alice_physics::joint::solve_joints(&self.joints, &mut self.bodies, dt);
}
fn recv_body_positions(&self, p: &mut [[Fix128; 3]]) {
for (out, b) in p.iter_mut().zip(&self.bodies) {
*out = [b.position.x, b.position.y, b.position.z];
}
}
}
fn limited_hinge_world() -> PhysicsWorld {
use alice_physics::joint::{HingeJoint, Joint};
let mut w = PhysicsWorld::new(weightless());
w.add_body(RigidBody::new(Vec3Fix::ZERO, Fix128::ONE));
let b = w.add_body(RigidBody::new(Vec3Fix::ZERO, Fix128::ONE));
w.get_body_mut(b)
.unwrap()
.set_rotation(QuatFix::from_axis_angle(Vec3Fix::UNIT_Z, Fix128::ONE));
w.add_joint(Joint::Hinge(
HingeJoint::new(
0,
1,
Vec3Fix::ZERO,
Vec3Fix::ZERO,
Vec3Fix::UNIT_Z,
Vec3Fix::UNIT_Z,
)
.with_limits(Fix128::from_ratio(-1, 2), Fix128::from_ratio(1, 2)),
));
w
}
#[test]
fn cpu_joint_solve_corrects_a_hinge_limit_by_rotating_the_bodies() {
let mut w = limited_hinge_world();
let before = w.bodies[1].rotation;
alice_physics::joint::solve_joints(&w.joints.clone(), &mut w.bodies, dt());
assert_ne!(w.bodies[0].rotation, QuatFix::IDENTITY, "body A rotated");
assert_ne!(w.bodies[1].rotation, before, "body B rotated");
}
#[test]
#[ignore = "known defect: AUD-A-S1W2-006: solve_joints_with_bridge writes back positions only; the rotation corrections of the CPU joint solve (joint.rs apply_angular_correction) cannot come back through GpuSolverBridge, so a hinge limit is not enforced under a bridge"]
fn joint_solve_through_a_reference_bridge_matches_the_cpu_solve() {
let mut cpu = limited_hinge_world();
alice_physics::joint::solve_joints(&cpu.joints.clone(), &mut cpu.bodies, dt());
let mut routed = limited_hinge_world();
let mut bridge = JointReference::default();
routed.solve_joints_with_bridge(&mut bridge, dt());
assert_eq!(routed.bodies[0].rotation, cpu.bodies[0].rotation, "body A");
assert_eq!(routed.bodies[1].rotation, cpu.bodies[1].rotation, "body B");
}
}
#[test]
fn add_joint_panics_when_either_index_is_out_of_bounds() {
use alice_physics::joint::{BallJoint, Joint};
let ball = |a: usize, b: usize| Joint::Ball(BallJoint::new(a, b, Vec3Fix::ZERO, Vec3Fix::ZERO));
let mut w = PhysicsWorld::new(weightless());
w.add_body(RigidBody::new(Vec3Fix::ZERO, Fix128::ONE));
w.add_body(RigidBody::new(Vec3Fix::ZERO, Fix128::ONE));
assert_eq!(w.add_joint(ball(0, 1)), 0);
for (a, b) in [(0, 2), (2, 0), (5, 7)] {
let r = catch_unwind(AssertUnwindSafe(|| {
let mut w2 = PhysicsWorld::new(weightless());
w2.add_body(RigidBody::new(Vec3Fix::ZERO, Fix128::ONE));
w2.add_body(RigidBody::new(Vec3Fix::ZERO, Fix128::ONE));
w2.add_joint(ball(a, b));
}));
assert!(r.is_err(), "joint ({a}, {b}) must panic");
}
}
#[test]
fn wake_body_one_past_the_last_body_is_ignored() {
let mut w = PhysicsWorld::new(weightless());
w.add_body(RigidBody::new(Vec3Fix::ZERO, Fix128::ONE));
w.add_body(RigidBody::new(Vec3Fix::ZERO, Fix128::ONE));
let r = catch_unwind(AssertUnwindSafe(|| w.wake_body(2)));
assert!(
r.is_ok(),
"wake_body(body_count) must be ignored, not panic"
);
}
#[test]
fn touching_spheres_are_clear_and_a_hair_closer_overlap() {
let mut w = PhysicsWorld::new(weightless());
let a = w.add_body_with_radius(RigidBody::new(v3(0.0, 0.0, 0.0), Fix128::ONE), fx(1.0));
let b = w.add_body_with_radius(RigidBody::new(v3(3.0, 0.0, 0.0), Fix128::ONE), fx(2.0));
assert!(!w.colliders_overlap(a, b) && !w.colliders_overlap(b, a));
w.get_body_mut(b).unwrap().set_position(v3(2.999, 0.0, 0.0));
assert!(w.colliders_overlap(a, b) && w.colliders_overlap(b, a));
}
#[test]
fn remove_body_keeps_each_survivors_filter() {
let mut w = PhysicsWorld::new(weightless());
let f0 = CollisionFilter::new(1, 0xF);
let f1 = CollisionFilter::new(2, 0xE);
let f2 = CollisionFilter::new(4, 0xD);
for f in [f0, f1, f2] {
let i = w.add_body(RigidBody::new(Vec3Fix::ZERO, Fix128::ONE));
w.set_body_filter(i, f);
}
w.remove_body(0);
assert_eq!(w.body_filter(0), f2, "last body moved into slot 0");
assert_eq!(w.body_filter(1), f1);
assert_eq!(w.body_filter(2), CollisionFilter::DEFAULT, "past the end");
}
#[test]
fn body_restitution_changes_the_bounce() {
let run = |e: f64| {
let mut w = PhysicsWorld::new(weightless());
let a = w.add_body_with_radius(
RigidBody::new(v3(0.0, 0.0, 0.0), Fix128::ONE)
.with_velocity(v3(20.0, 0.0, 0.0))
.with_restitution(fx(e)),
Fix128::ONE,
);
w.add_body_with_radius(
RigidBody::new(v3(2.1, 0.0, 0.0), Fix128::ONE)
.with_velocity(v3(-20.0, 0.0, 0.0))
.with_restitution(fx(e)),
Fix128::ONE,
);
w.step(dt());
w.get_body(a).unwrap().velocity.x.to_f64()
};
let (stick, bounce) = (run(0.0), run(1.0));
assert!(
(stick - bounce).abs() > 1.0,
"restitution 0 -> vx {stick}, restitution 1 -> vx {bounce}: no effect"
);
}
#[test]
fn body_defaults_equal_the_default_material() {
let w = PhysicsWorld::new(weightless());
let m = w.material_table.combine(0, 0);
for b in [
RigidBody::new(Vec3Fix::ZERO, Fix128::ONE),
RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE),
RigidBody::new_static(Vec3Fix::ZERO),
RigidBody::new_kinematic(Vec3Fix::ZERO),
] {
assert_eq!((b.friction, b.restitution), (m.friction, m.restitution));
}
}
#[test]
fn combined_material_reads_default_material_bodies_values() {
let mut w = PhysicsWorld::new(weightless());
let a = w.add_body(RigidBody::new(Vec3Fix::ZERO, Fix128::ONE));
let b = w.add_body(
RigidBody::new(Vec3Fix::ZERO, Fix128::ONE)
.with_friction(fx(0.9))
.with_restitution(fx(0.1)),
);
assert_eq!(w.combined_material(a, a), w.material_table.combine(0, 0));
let c = w.combined_material(a, b);
close(c.friction, 0.7, 1e-12, "friction (0.5 + 0.9) / 2");
close(c.restitution, 0.2, 1e-12, "restitution (0.3 + 0.1) / 2");
}
#[test]
fn combined_material_ignores_body_values_of_a_body_with_a_material() {
let mut w = PhysicsWorld::new(weightless());
let metal = w.material_table.register_metal();
let a = w.add_body(
RigidBody::new(Vec3Fix::ZERO, Fix128::ONE)
.with_friction(fx(0.9))
.with_restitution(fx(0.9)),
);
let b = w.add_body(RigidBody::new(Vec3Fix::ZERO, Fix128::ONE));
w.set_body_material(a, metal);
assert_eq!(
w.combined_material(a, b),
w.material_table.combine(metal, 0)
);
}