#![allow(clippy::ptr_arg)]
#![allow(dead_code)]
#![allow(clippy::too_many_arguments)]
use crate::math::Real;
use crate::types::{Aabb, TimeStep, Transform};
pub trait Steppable {
fn step(&mut self, time_step: &TimeStep);
}
pub trait PhysicsBody {
fn transform(&self) -> &Transform;
fn transform_mut(&mut self) -> &mut Transform;
fn mass(&self) -> Real;
fn inv_mass(&self) -> Real {
if self.mass() > 0.0 {
1.0 / self.mass()
} else {
0.0
}
}
fn is_dynamic(&self) -> bool {
self.mass() > 0.0
}
fn is_static(&self) -> bool {
self.mass() == 0.0
}
}
pub trait Collidable {
fn bounding_box(&self) -> Aabb;
fn can_collide_with(&self, _other: &dyn Collidable) -> bool {
true
}
}
pub trait ForceSource {
fn force_at(
&self,
transform: &Transform,
velocity: [Real; 3],
mass: Real,
) -> ([Real; 3], [Real; 3]);
}
pub trait PhysicsConstraint {
fn dof(&self) -> usize;
fn positional_error(&self) -> Vec<Real>;
fn is_active(&self) -> bool;
fn compliance(&self) -> Real {
0.0
}
}
pub trait Integrator {
fn integrate(&self, q: &mut [Real], v: &mut [Real], f: &[Real], inv_mass: &[Real], dt: Real);
}
pub struct SemiImplicitEuler;
impl Integrator for SemiImplicitEuler {
fn integrate(&self, q: &mut [Real], v: &mut [Real], f: &[Real], inv_mass: &[Real], dt: Real) {
let n = q.len().min(v.len()).min(f.len()).min(inv_mass.len());
for i in 0..n {
v[i] += f[i] * inv_mass[i] * dt;
q[i] += v[i] * dt;
}
}
}
pub struct ExplicitEuler;
impl Integrator for ExplicitEuler {
fn integrate(&self, q: &mut [Real], v: &mut [Real], f: &[Real], inv_mass: &[Real], dt: Real) {
let n = q.len().min(v.len()).min(f.len()).min(inv_mass.len());
for i in 0..n {
q[i] += v[i] * dt;
v[i] += f[i] * inv_mass[i] * dt;
}
}
}
pub struct RungeKutta4;
impl RungeKutta4 {
pub fn integrate_rk4(q: &mut [Real], v: &mut [Real], accel: &[Real], dt: Real) {
let n = q.len().min(v.len()).min(accel.len());
let h = dt;
let h2 = h / 2.0;
let h6 = h / 6.0;
for i in 0..n {
let a = accel[i];
let kv1 = a;
let kv2 = a; let kv3 = a;
let kv4 = a;
let kq1 = v[i];
let kq2 = v[i] + h2 * kv1;
let kq3 = v[i] + h2 * kv2;
let kq4 = v[i] + h * kv3;
v[i] += h6 * (kv1 + 2.0 * kv2 + 2.0 * kv3 + kv4);
q[i] += h6 * (kq1 + 2.0 * kq2 + 2.0 * kq3 + kq4);
}
}
}
impl Integrator for RungeKutta4 {
fn integrate(&self, q: &mut [Real], v: &mut [Real], f: &[Real], inv_mass: &[Real], dt: Real) {
let n = q.len().min(v.len()).min(f.len()).min(inv_mass.len());
let accel: Vec<Real> = (0..n).map(|i| f[i] * inv_mass[i]).collect();
Self::integrate_rk4(q, v, &accel, dt);
}
}
pub trait Broadphase {
fn insert(&mut self, id: u64, aabb: Aabb);
fn update(&mut self, id: u64, aabb: Aabb);
fn remove(&mut self, id: u64);
fn overlapping_pairs(&self) -> Vec<(u64, u64)>;
fn query_aabb(&self, query: &Aabb) -> Vec<u64>;
}
#[derive(Debug, Clone)]
pub struct ContactPoint {
pub point: [Real; 3],
pub normal: [Real; 3],
pub depth: Real,
}
pub trait Narrowphase {
fn test_overlap(&self, transform_a: &Transform, transform_b: &Transform) -> bool;
fn contacts(&self, transform_a: &Transform, transform_b: &Transform) -> Vec<ContactPoint>;
}
pub trait PhysicsStateful {
type State;
fn save_state(&self) -> Self::State;
fn restore_state(&mut self, state: Self::State);
}
pub trait Damped {
fn linear_damping(&self) -> Real;
fn angular_damping(&self) -> Real;
fn damp_velocity(&self, velocity: Real, dt: Real, is_angular: bool) -> Real {
let d = if is_angular {
self.angular_damping()
} else {
self.linear_damping()
};
velocity * (1.0 - d * dt).max(0.0)
}
}
pub trait Sleepable {
fn is_sleeping(&self) -> bool;
fn wake(&mut self);
fn sleep(&mut self);
fn should_sleep(
&self,
speed_sq: Real,
threshold_sq: Real,
accumulated_sleep_time: Real,
time_to_sleep: Real,
) -> bool {
speed_sq < threshold_sq && accumulated_sleep_time >= time_to_sleep
}
}
pub trait Renderable {
fn render_transform(&self, alpha: Real) -> Transform;
}
#[derive(Debug, Clone)]
pub struct PhysicsMaterial {
pub static_friction: Real,
pub dynamic_friction: Real,
pub restitution: Real,
pub rolling_friction: Real,
}
impl Default for PhysicsMaterial {
fn default() -> Self {
Self {
static_friction: 0.5,
dynamic_friction: 0.4,
restitution: 0.3,
rolling_friction: 0.01,
}
}
}
impl PhysicsMaterial {
pub fn rubber() -> Self {
Self {
static_friction: 1.0,
dynamic_friction: 0.8,
restitution: 0.7,
rolling_friction: 0.02,
}
}
pub fn steel() -> Self {
Self {
static_friction: 0.74,
dynamic_friction: 0.57,
restitution: 0.6,
rolling_friction: 0.001,
}
}
pub fn ice() -> Self {
Self {
static_friction: 0.03,
dynamic_friction: 0.02,
restitution: 0.05,
rolling_friction: 0.001,
}
}
pub fn combine(&self, other: &PhysicsMaterial) -> PhysicsMaterial {
PhysicsMaterial {
static_friction: (self.static_friction * other.static_friction).sqrt(),
dynamic_friction: (self.dynamic_friction * other.dynamic_friction).sqrt(),
restitution: self.restitution.min(other.restitution),
rolling_friction: (self.rolling_friction * other.rolling_friction).sqrt(),
}
}
}
pub trait HasMaterial {
fn material(&self) -> &PhysicsMaterial;
}
pub trait PhysicsDiagnostics {
fn diagnostics(&self) -> Vec<(&'static str, Real)>;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::VelocityVerlet;
use crate::fabrik_solve;
#[test]
fn test_semi_implicit_euler_constant_force() {
let integrator = SemiImplicitEuler;
let mut q = [0.0f64];
let mut v = [0.0f64];
let f = [1.0f64]; let inv_mass = [1.0f64]; integrator.integrate(&mut q, &mut v, &f, &inv_mass, 1.0);
assert!((v[0] - 1.0).abs() < 1e-12, "v={}", v[0]);
assert!((q[0] - 1.0).abs() < 1e-12, "q={}", q[0]);
}
#[test]
fn test_semi_implicit_euler_zero_force() {
let integrator = SemiImplicitEuler;
let mut q = [5.0f64];
let mut v = [2.0f64];
let f = [0.0f64];
let inv_mass = [1.0f64];
integrator.integrate(&mut q, &mut v, &f, &inv_mass, 0.5);
assert!((v[0] - 2.0).abs() < 1e-12);
assert!((q[0] - 6.0).abs() < 1e-12);
}
#[test]
fn test_explicit_euler_differs_from_semi_implicit() {
let f = [2.0f64];
let inv_mass = [1.0f64];
let dt = 1.0;
let mut q_si = [0.0f64];
let mut v_si = [0.0f64];
SemiImplicitEuler.integrate(&mut q_si, &mut v_si, &f, &inv_mass, dt);
let mut q_ex = [0.0f64];
let mut v_ex = [0.0f64];
ExplicitEuler.integrate(&mut q_ex, &mut v_ex, &f, &inv_mass, dt);
assert!((v_si[0] - 2.0).abs() < 1e-12);
assert!((q_si[0] - 2.0).abs() < 1e-12);
assert!((v_ex[0] - 2.0).abs() < 1e-12);
assert!((q_ex[0] - 0.0).abs() < 1e-12); }
#[test]
fn test_rk4_constant_acceleration() {
let mut q = [0.0f64];
let mut v = [0.0f64];
let a = [2.0f64];
let inv_mass = [1.0f64];
RungeKutta4.integrate(&mut q, &mut v, &a, &inv_mass, 1.0);
assert!((v[0] - 2.0).abs() < 1e-10, "v={}", v[0]);
assert!((q[0] - 1.0).abs() < 1e-10, "q={}", q[0]);
}
#[test]
fn test_rk4_with_initial_velocity() {
let mut q = [0.0f64];
let mut v = [3.0f64];
let a = [0.0f64]; let inv_mass = [1.0f64];
RungeKutta4.integrate(&mut q, &mut v, &a, &inv_mass, 2.0);
assert!((q[0] - 6.0).abs() < 1e-10, "q={}", q[0]);
assert!((v[0] - 3.0).abs() < 1e-10, "v={}", v[0]);
}
#[test]
fn test_material_combine_friction_geometric_mean() {
let a = PhysicsMaterial {
static_friction: 4.0,
dynamic_friction: 4.0,
restitution: 0.5,
rolling_friction: 0.01,
};
let b = PhysicsMaterial {
static_friction: 1.0,
dynamic_friction: 1.0,
restitution: 0.8,
rolling_friction: 0.01,
};
let c = a.combine(&b);
assert!((c.static_friction - 2.0).abs() < 1e-12);
assert!((c.dynamic_friction - 2.0).abs() < 1e-12);
assert!((c.restitution - 0.5).abs() < 1e-12);
}
#[test]
fn test_material_presets_valid() {
let rubber = PhysicsMaterial::rubber();
assert!(rubber.static_friction > 0.0);
assert!(rubber.restitution <= 1.0);
let steel = PhysicsMaterial::steel();
assert!(steel.dynamic_friction > 0.0);
let ice = PhysicsMaterial::ice();
assert!(ice.static_friction < 0.1);
}
struct SimpleDamped {
lin: Real,
ang: Real,
}
impl Damped for SimpleDamped {
fn linear_damping(&self) -> Real {
self.lin
}
fn angular_damping(&self) -> Real {
self.ang
}
}
#[test]
fn test_damped_velocity() {
let d = SimpleDamped { lin: 0.5, ang: 0.2 };
let v_after = d.damp_velocity(10.0, 1.0, false);
assert!((v_after - 5.0).abs() < 1e-12, "v={}", v_after);
}
#[test]
fn test_damped_velocity_clamp_zero() {
let d = SimpleDamped { lin: 2.0, ang: 0.0 };
let v_after = d.damp_velocity(10.0, 1.0, false);
assert_eq!(v_after, 0.0);
}
#[test]
fn test_sleepable_should_sleep() {
struct DummySleepable;
impl Sleepable for DummySleepable {
fn is_sleeping(&self) -> bool {
false
}
fn wake(&mut self) {}
fn sleep(&mut self) {}
}
let s = DummySleepable;
assert!(s.should_sleep(0.0001, 0.01, 1.0, 0.5));
assert!(!s.should_sleep(0.1, 0.01, 1.0, 0.5)); assert!(!s.should_sleep(0.0001, 0.01, 0.1, 0.5)); }
#[test]
fn test_contact_point_construction() {
let cp = ContactPoint {
point: [1.0, 2.0, 3.0],
normal: [0.0, 1.0, 0.0],
depth: 0.05,
};
assert!((cp.depth - 0.05).abs() < 1e-12);
assert_eq!(cp.normal[1], 1.0);
}
#[test]
fn test_integrator_length_mismatch_safe() {
let integrator = SemiImplicitEuler;
let mut q = vec![0.0, 0.0, 0.0];
let mut v = vec![1.0, 1.0];
let f = vec![0.0, 0.0, 0.0];
let inv_mass = vec![1.0, 1.0, 1.0];
integrator.integrate(&mut q, &mut v, &f, &inv_mass, 0.1);
assert_eq!(q[2], 0.0); }
#[test]
fn test_velocity_verlet_constant_accel() {
let mut q = [0.0f64];
let mut v = [0.0f64];
let a = [2.0f64];
let dt = 1.0;
VelocityVerlet.integrate(&mut q, &mut v, &a, &[1.0], dt);
assert!((q[0] - 1.0).abs() < 1e-12, "q={}", q[0]);
assert!((v[0] - 2.0).abs() < 1e-12, "v={}", v[0]);
}
#[test]
fn test_leapfrog_energy_conservation() {
let omega2 = 1.0f64; let dt = 0.01;
let n_steps = 1000;
let mut x = 1.0f64;
let mut v = 0.0f64;
let a0 = -omega2 * x;
let mut v_half = v + 0.5 * a0 * dt;
let e0 = 0.5 * v * v + 0.5 * x * x;
for _ in 0..n_steps {
x += v_half * dt;
let a = -omega2 * x;
v_half += a * dt;
v = v_half - 0.5 * a * dt;
}
let e_final = 0.5 * v * v + 0.5 * x * x;
assert!((e_final - e0).abs() < 0.01, "E0={e0}, Ef={e_final}");
}
#[test]
fn test_velocity_verlet_free_fall() {
let g = -9.81f64;
let dt = 0.001;
let n = 1000;
let mut y = 0.0f64;
let mut vy = 0.0f64;
let mut ay = g;
for _ in 0..n {
y += vy * dt + 0.5 * ay * dt * dt;
let ay_new = g;
vy += 0.5 * (ay + ay_new) * dt;
ay = ay_new;
}
let t = dt * n as f64;
let expected = 0.5 * g * t * t;
assert!((y - expected).abs() < 1e-8, "y={y}, expected={expected}");
}
#[test]
fn test_rkf45_step_count_positive() {
let mut y = [1.0f64];
let a = [0.0f64];
let inv_mass = [1.0f64];
RungeKutta4.integrate(&mut y, &mut a.clone(), &a, &inv_mass, 0.1);
assert!(y[0].is_finite());
}
#[test]
fn test_material_combine_zero_friction() {
let a = PhysicsMaterial {
static_friction: 0.0,
dynamic_friction: 0.0,
restitution: 0.5,
rolling_friction: 0.0,
};
let b = PhysicsMaterial::rubber();
let c = a.combine(&b);
assert_eq!(c.static_friction, 0.0);
assert_eq!(c.dynamic_friction, 0.0);
}
#[test]
fn test_material_combine_min_restitution() {
let a = PhysicsMaterial {
restitution: 0.9,
..PhysicsMaterial::default()
};
let b = PhysicsMaterial {
restitution: 0.1,
..PhysicsMaterial::default()
};
let c = a.combine(&b);
assert!((c.restitution - 0.1).abs() < 1e-12);
}
#[test]
fn test_fabrik_reaches_target_1dof() {
let bones = vec![[0.0f64, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
let lengths = vec![1.0f64, 1.0];
let target = [1.0, 0.5, 0.0]; let result = fabrik_solve(&bones, &lengths, &target, 50, 1e-6);
let ee = result.last().unwrap();
let d = ((ee[0] - target[0]).powi(2)
+ (ee[1] - target[1]).powi(2)
+ (ee[2] - target[2]).powi(2))
.sqrt();
assert!(d < 0.01, "FABRIK end effector distance to target: {d}");
}
#[test]
fn test_fabrik_unreachable_extends_toward_target() {
let bones = vec![[0.0f64, 0.0, 0.0], [1.0, 0.0, 0.0]];
let lengths = vec![1.0f64];
let target = [3.0, 0.0, 0.0];
let result = fabrik_solve(&bones, &lengths, &target, 20, 1e-6);
let ee = result.last().unwrap();
assert!(
ee[0] > 0.9,
"end effector should extend toward target: x={}",
ee[0]
);
}
}
pub struct VelocityVerlet;
impl Integrator for VelocityVerlet {
fn integrate(&self, q: &mut [Real], v: &mut [Real], f: &[Real], inv_mass: &[Real], dt: Real) {
let n = q.len().min(v.len()).min(f.len()).min(inv_mass.len());
let dt2 = dt * dt;
for i in 0..n {
let a = f[i] * inv_mass[i];
q[i] += v[i] * dt + 0.5 * a * dt2;
v[i] += a * dt;
}
}
}
pub struct Leapfrog;
impl Integrator for Leapfrog {
fn integrate(&self, q: &mut [Real], v: &mut [Real], f: &[Real], inv_mass: &[Real], dt: Real) {
let n = q.len().min(v.len()).min(f.len()).min(inv_mass.len());
for i in 0..n {
let a = f[i] * inv_mass[i];
let v_half = v[i] + a * dt * 0.5;
q[i] += v_half * dt;
v[i] = v_half + a * dt * 0.5;
}
}
}
pub struct AdamsBashforth2 {
pub f_prev: Option<Vec<Real>>,
}
impl AdamsBashforth2 {
pub fn new() -> Self {
Self { f_prev: None }
}
pub fn reset(&mut self) {
self.f_prev = None;
}
pub fn integrate_ab2(
&mut self,
q: &mut [Real],
v: &mut [Real],
f: &[Real],
inv_mass: &[Real],
dt: Real,
) {
let n = q.len().min(v.len()).min(f.len()).min(inv_mass.len());
match &self.f_prev {
None => {
for i in 0..n {
let a = f[i] * inv_mass[i];
v[i] += a * dt;
q[i] += v[i] * dt;
}
}
Some(fp) => {
let m = fp.len().min(n);
for i in 0..m {
let a_curr = f[i] * inv_mass[i];
let a_prev = fp[i] * inv_mass[i];
let a_eff = 1.5 * a_curr - 0.5 * a_prev;
v[i] += a_eff * dt;
q[i] += v[i] * dt;
}
}
}
self.f_prev = Some(f[..n].to_vec());
}
}
impl Default for AdamsBashforth2 {
fn default() -> Self {
Self::new()
}
}
impl Integrator for AdamsBashforth2 {
fn integrate(&self, q: &mut [Real], v: &mut [Real], f: &[Real], inv_mass: &[Real], dt: Real) {
let n = q.len().min(v.len()).min(f.len()).min(inv_mass.len());
for i in 0..n {
let a = f[i] * inv_mass[i];
v[i] += a * dt;
q[i] += v[i] * dt;
}
}
}
pub struct ImplicitEuler;
impl Integrator for ImplicitEuler {
fn integrate(&self, q: &mut [Real], v: &mut [Real], f: &[Real], inv_mass: &[Real], dt: Real) {
let n = q.len().min(v.len()).min(f.len()).min(inv_mass.len());
for i in 0..n {
v[i] += f[i] * inv_mass[i] * dt;
q[i] += v[i] * dt;
}
}
}
pub struct Midpoint;
impl Integrator for Midpoint {
fn integrate(&self, q: &mut [Real], v: &mut [Real], f: &[Real], inv_mass: &[Real], dt: Real) {
let n = q.len().min(v.len()).min(f.len()).min(inv_mass.len());
let h2 = dt / 2.0;
for i in 0..n {
let a = f[i] * inv_mass[i];
let v_mid = v[i] + a * h2;
v[i] += a * dt;
q[i] += v_mid * dt;
}
}
}
pub fn xpbd_distance_correction(
p_a: [Real; 3],
p_b: [Real; 3],
inv_mass_a: Real,
inv_mass_b: Real,
rest_length: Real,
compliance: Real,
dt: Real,
) -> ([Real; 3], [Real; 3]) {
let dx = [p_b[0] - p_a[0], p_b[1] - p_a[1], p_b[2] - p_a[2]];
let dist = (dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2]).sqrt();
if dist < 1e-12 {
return ([0.0; 3], [0.0; 3]);
}
let n = [dx[0] / dist, dx[1] / dist, dx[2] / dist];
let c = dist - rest_length;
let alpha = compliance / (dt * dt);
let w = inv_mass_a + inv_mass_b;
if w < 1e-12 {
return ([0.0; 3], [0.0; 3]);
}
let lambda = -c / (w + alpha);
let da = [
-lambda * inv_mass_a * n[0],
-lambda * inv_mass_a * n[1],
-lambda * inv_mass_a * n[2],
];
let db = [
lambda * inv_mass_b * n[0],
lambda * inv_mass_b * n[1],
lambda * inv_mass_b * n[2],
];
(da, db)
}
#[allow(clippy::too_many_arguments)]
pub fn contact_impulse_scalar(
v_rel_n: Real,
inv_mass_a: Real,
inv_mass_b: Real,
inv_inertia_a: Real,
inv_inertia_b: Real,
r_a: [Real; 3],
r_b: [Real; 3],
normal: [Real; 3],
) -> Real {
fn cross_sq(r: [Real; 3], n: [Real; 3]) -> Real {
let cx = r[1] * n[2] - r[2] * n[1];
let cy = r[2] * n[0] - r[0] * n[2];
let cz = r[0] * n[1] - r[1] * n[0];
cx * cx + cy * cy + cz * cz
}
let k = inv_mass_a
+ inv_mass_b
+ inv_inertia_a * cross_sq(r_a, normal)
+ inv_inertia_b * cross_sq(r_b, normal);
if k < 1e-12 { 0.0 } else { -v_rel_n / k }
}
pub fn fabrik_solve(
bones: &[[Real; 3]],
lengths: &[Real],
target: &[Real; 3],
max_iter: usize,
tolerance: Real,
) -> Vec<[Real; 3]> {
let n = bones.len();
if n < 2 || lengths.len() < n - 1 {
return bones.to_vec();
}
let mut joints: Vec<[Real; 3]> = bones.to_vec();
let root = joints[0];
let total_length: Real = lengths.iter().sum();
let dist_to_target = dist3(&joints[0], target);
if dist_to_target >= total_length {
for i in 0..n - 1 {
let d = dist3(&joints[i], target);
let t = lengths[i] / d;
joints[i + 1] = lerp3(&joints[i], target, t);
}
return joints;
}
for _ in 0..max_iter {
joints[n - 1] = *target;
for i in (0..n - 1).rev() {
let d = dist3(&joints[i + 1], &joints[i]);
if d < 1e-12 {
continue;
}
let t = lengths[i] / d;
joints[i] = lerp3(&joints[i + 1], &joints[i], t);
}
joints[0] = root;
for i in 0..n - 1 {
let d = dist3(&joints[i], &joints[i + 1]);
if d < 1e-12 {
continue;
}
let t = lengths[i] / d;
joints[i + 1] = lerp3(&joints[i], &joints[i + 1], t);
}
let ee_dist = dist3(&joints[n - 1], target);
if ee_dist < tolerance {
break;
}
}
joints
}
fn dist3(a: &[Real; 3], b: &[Real; 3]) -> Real {
let dx = b[0] - a[0];
let dy = b[1] - a[1];
let dz = b[2] - a[2];
(dx * dx + dy * dy + dz * dz).sqrt()
}
fn lerp3(a: &[Real; 3], b: &[Real; 3], t: Real) -> [Real; 3] {
[
a[0] + t * (b[0] - a[0]),
a[1] + t * (b[1] - a[1]),
a[2] + t * (b[2] - a[2]),
]
}
pub trait PbdConstraint {
fn evaluate(&self, positions: &[[Real; 3]]) -> Real;
fn gradient(&self, positions: &[[Real; 3]]) -> Vec<[Real; 3]>;
fn particle_indices(&self) -> &[usize];
fn stiffness(&self) -> Real {
1.0
}
}
pub struct PbdDistanceConstraint {
pub indices: [usize; 2],
pub rest_length: Real,
pub k: Real,
}
impl PbdDistanceConstraint {
pub fn new(i: usize, j: usize, rest_length: Real, stiffness: Real) -> Self {
Self {
indices: [i, j],
rest_length,
k: stiffness,
}
}
}
impl PbdConstraint for PbdDistanceConstraint {
fn evaluate(&self, positions: &[[Real; 3]]) -> Real {
let [i, j] = self.indices;
if i >= positions.len() || j >= positions.len() {
return 0.0;
}
let p = &positions[i];
let q = &positions[j];
dist3(&[p[0], p[1], p[2]], &[q[0], q[1], q[2]]) - self.rest_length
}
fn gradient(&self, positions: &[[Real; 3]]) -> Vec<[Real; 3]> {
let [i, j] = self.indices;
if i >= positions.len() || j >= positions.len() {
return vec![[0.0; 3]; 2];
}
let p = positions[i];
let q = positions[j];
let dx = [q[0] - p[0], q[1] - p[1], q[2] - p[2]];
let d = (dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2]).sqrt();
if d < 1e-12 {
return vec![[0.0; 3]; 2];
}
let n = [dx[0] / d, dx[1] / d, dx[2] / d];
vec![[-n[0], -n[1], -n[2]], [n[0], n[1], n[2]]]
}
fn particle_indices(&self) -> &[usize] {
&self.indices
}
fn stiffness(&self) -> Real {
self.k
}
}
pub fn pbd_solve_iteration(
positions: &mut Vec<[Real; 3]>,
inv_masses: &[Real],
constraints: &[&dyn PbdConstraint],
) {
for c in constraints {
let c_val = c.evaluate(positions);
let grads = c.gradient(positions);
let indices = c.particle_indices();
if grads.len() < indices.len() {
continue;
}
let mut denom = 0.0;
for (k, &idx) in indices.iter().enumerate() {
if idx < inv_masses.len() {
let g = grads[k];
denom += inv_masses[idx] * (g[0] * g[0] + g[1] * g[1] + g[2] * g[2]);
}
}
if denom < 1e-12 {
continue;
}
let lambda = -c_val / denom * c.stiffness();
for (k, &idx) in indices.iter().enumerate() {
if idx < positions.len() && idx < inv_masses.len() {
let g = grads[k];
let w = inv_masses[idx];
positions[idx][0] += lambda * w * g[0];
positions[idx][1] += lambda * w * g[1];
positions[idx][2] += lambda * w * g[2];
}
}
}
}
pub trait CollisionFilter {
fn should_collide(&self, id_a: u64, id_b: u64) -> bool;
}
pub struct LayerFilter {
pub layers: std::collections::HashMap<u64, u32>,
pub masks: std::collections::HashMap<u64, u32>,
}
impl LayerFilter {
pub fn new() -> Self {
Self {
layers: std::collections::HashMap::new(),
masks: std::collections::HashMap::new(),
}
}
pub fn register(&mut self, id: u64, layer: u32, mask: u32) {
self.layers.insert(id, layer);
self.masks.insert(id, mask);
}
}
impl Default for LayerFilter {
fn default() -> Self {
Self::new()
}
}
impl CollisionFilter for LayerFilter {
fn should_collide(&self, id_a: u64, id_b: u64) -> bool {
let layer_a = self.layers.get(&id_a).copied().unwrap_or(0xFFFF_FFFF);
let layer_b = self.layers.get(&id_b).copied().unwrap_or(0xFFFF_FFFF);
let mask_a = self.masks.get(&id_a).copied().unwrap_or(0xFFFF_FFFF);
let mask_b = self.masks.get(&id_b).copied().unwrap_or(0xFFFF_FFFF);
(mask_a & layer_b) != 0 && (mask_b & layer_a) != 0
}
}
pub trait PhysicsSensor {
fn is_triggered(&self) -> bool;
fn overlapping_bodies(&self) -> Vec<u64>;
fn update(&mut self, body_positions: &[(u64, [Real; 3])]);
}
pub struct SphereSensor {
pub center: [Real; 3],
pub radius: Real,
pub current_overlaps: Vec<u64>,
}
impl SphereSensor {
pub fn new(center: [Real; 3], radius: Real) -> Self {
Self {
center,
radius,
current_overlaps: Vec::new(),
}
}
}
impl PhysicsSensor for SphereSensor {
fn is_triggered(&self) -> bool {
!self.current_overlaps.is_empty()
}
fn overlapping_bodies(&self) -> Vec<u64> {
self.current_overlaps.clone()
}
fn update(&mut self, body_positions: &[(u64, [Real; 3])]) {
let r2 = self.radius * self.radius;
self.current_overlaps = body_positions
.iter()
.filter_map(|(id, pos)| {
let dx = pos[0] - self.center[0];
let dy = pos[1] - self.center[1];
let dz = pos[2] - self.center[2];
if dx * dx + dy * dy + dz * dz <= r2 {
Some(*id)
} else {
None
}
})
.collect();
}
}
pub trait PhysicsInterpolatable: Sized {
fn lerp_state(&self, other: &Self, t: Real) -> Self;
}
#[cfg(test)]
mod expanded_tests {
use super::*;
use crate::AdamsBashforth2;
use crate::LayerFilter;
use crate::Leapfrog;
use crate::Midpoint;
use crate::PbdConstraint;
use crate::PbdDistanceConstraint;
use crate::SphereSensor;
use crate::VelocityVerlet;
use crate::dist3;
use crate::fabrik_solve;
use crate::pbd_solve_iteration;
use crate::xpbd_distance_correction;
#[test]
fn test_velocity_verlet_no_force() {
let mut q = [3.0f64];
let mut v = [2.0f64];
VelocityVerlet.integrate(&mut q, &mut v, &[0.0], &[1.0], 0.5);
assert!((q[0] - 4.0).abs() < 1e-12, "q={}", q[0]);
assert!((v[0] - 2.0).abs() < 1e-12, "v={}", v[0]);
}
#[test]
fn test_leapfrog_rest() {
let mut q = [1.0f64];
let mut v = [0.0f64];
Leapfrog.integrate(&mut q, &mut v, &[0.0], &[1.0], 1.0);
assert!(
(q[0] - 1.0).abs() < 1e-12,
"q should not change: q={}",
q[0]
);
}
#[test]
fn test_midpoint_position_uses_midpoint_velocity() {
let mut q = [0.0f64];
let mut v = [0.0f64];
Midpoint.integrate(&mut q, &mut v, &[4.0], &[1.0], 1.0);
assert!((q[0] - 2.0).abs() < 1e-12, "q={}", q[0]);
assert!((v[0] - 4.0).abs() < 1e-12, "v={}", v[0]);
}
#[test]
fn test_ab2_bootstrap_euler() {
let mut ab2 = AdamsBashforth2::new();
let mut q = [0.0f64];
let mut v = [0.0f64];
ab2.integrate_ab2(&mut q, &mut v, &[2.0], &[1.0], 1.0);
assert!((v[0] - 2.0).abs() < 1e-12);
assert!((q[0] - 2.0).abs() < 1e-12);
}
#[test]
fn test_ab2_second_step() {
let mut ab2 = AdamsBashforth2::new();
let mut q = [0.0f64];
let mut v = [0.0f64];
ab2.integrate_ab2(&mut q, &mut v, &[2.0], &[1.0], 1.0);
ab2.integrate_ab2(&mut q, &mut v, &[2.0], &[1.0], 1.0);
assert!(v[0].is_finite());
assert!(q[0].is_finite());
}
#[test]
fn test_xpbd_distance_rigid() {
let pa = [0.0, 0.0, 0.0];
let pb = [2.0, 0.0, 0.0];
let (da, db) = xpbd_distance_correction(pa, pb, 1.0, 1.0, 1.0, 0.0, 0.01);
assert!(da[0] > 0.0, "da.x should be positive: {:?}", da);
assert!(db[0] < 0.0, "db.x should be negative: {:?}", db);
assert!((da[0].abs() - db[0].abs()).abs() < 1e-10);
}
#[test]
fn test_xpbd_distance_no_violation() {
let pa = [0.0, 0.0, 0.0];
let pb = [1.0, 0.0, 0.0];
let (da, db) = xpbd_distance_correction(pa, pb, 1.0, 1.0, 1.0, 0.0, 0.01);
assert!(da[0].abs() < 1e-12);
assert!(db[0].abs() < 1e-12);
}
#[test]
fn test_xpbd_static_body() {
let (da, db) =
xpbd_distance_correction([0.0, 0.0, 0.0], [2.0, 0.0, 0.0], 1.0, 0.0, 1.0, 0.0, 0.01);
assert!(da[0].abs() > 0.0);
assert_eq!(db[0], 0.0);
}
#[test]
fn test_pbd_distance_constraint_evaluate() {
let c = PbdDistanceConstraint::new(0, 1, 1.0, 1.0);
let positions = vec![[0.0, 0.0, 0.0f64], [2.0, 0.0, 0.0]];
let violation = c.evaluate(&positions);
assert!((violation - 1.0).abs() < 1e-12, "violation={}", violation);
}
#[test]
fn test_pbd_solve_iteration() {
let c = PbdDistanceConstraint::new(0, 1, 1.0, 1.0);
let constraints: Vec<&dyn PbdConstraint> = vec![&c];
let mut positions = vec![[0.0, 0.0, 0.0f64], [3.0, 0.0, 0.0]];
let inv_masses = [1.0f64, 1.0];
pbd_solve_iteration(&mut positions, &inv_masses, &constraints);
let d = (positions[1][0] - positions[0][0]).abs();
assert!(d < 3.0, "distance should decrease: {d}");
}
#[test]
fn test_layer_filter_collision() {
let mut filter = LayerFilter::new();
filter.register(1, 0b01, 0b10); filter.register(2, 0b10, 0b01); assert!(filter.should_collide(1, 2));
assert!(filter.should_collide(2, 1));
}
#[test]
fn test_layer_filter_no_collision() {
let mut filter = LayerFilter::new();
filter.register(1, 0b01, 0b01); filter.register(2, 0b10, 0b10); assert!(!filter.should_collide(1, 2));
}
#[test]
fn test_sphere_sensor_triggers() {
let mut sensor = SphereSensor::new([0.0; 3], 5.0);
let bodies = vec![(1u64, [1.0, 0.0, 0.0]), (2u64, [10.0, 0.0, 0.0])];
sensor.update(&bodies);
assert!(sensor.is_triggered());
assert_eq!(sensor.overlapping_bodies(), vec![1]);
}
#[test]
fn test_sphere_sensor_no_trigger() {
let mut sensor = SphereSensor::new([0.0; 3], 1.0);
let bodies = vec![(1u64, [5.0, 0.0, 0.0])];
sensor.update(&bodies);
assert!(!sensor.is_triggered());
}
#[test]
fn test_fabrik_2joint_chain() {
let bones = vec![[0.0, 0.0, 0.0f64], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
let lengths = vec![1.0f64, 1.0];
let target = [1.0, 1.0, 0.0];
let result = fabrik_solve(&bones, &lengths, &target, 50, 1e-6);
assert_eq!(result.len(), 3);
assert!((result[0][0]).abs() < 1e-10);
let ee = result[2];
let d = ((ee[0] - target[0]).powi(2) + (ee[1] - target[1]).powi(2)).sqrt();
assert!(d < 0.01, "end effector dist to target: {d}");
}
#[test]
fn test_fabrik_preserves_bone_lengths() {
let bones = vec![[0.0, 0.0, 0.0f64], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
let lengths = vec![1.0f64, 1.0];
let target = [0.5, 0.5, 0.0];
let result = fabrik_solve(&bones, &lengths, &target, 20, 1e-6);
for i in 0..2 {
let d = dist3(result[i], result[i + 1]);
assert!((d - lengths[i]).abs() < 1e-6, "bone {i} length: {d}");
}
}
}