use nalgebra::{Matrix3, UnitQuaternion, Vector3};
#[allow(unused_imports)]
use nalgebra::ComplexField;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BodyId(usize);
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum BodyType {
Dynamic,
Static,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Collider {
Sphere { radius: f32 },
Aabb { half_extents: Vector3<f32> },
Capsule { height: f32, radius: f32 },
}
#[derive(Debug, Clone)]
pub struct Contact {
pub body_a: BodyId,
pub body_b: BodyId,
pub normal: Vector3<f32>,
pub penetration: f32,
}
#[derive(Debug, Clone)]
pub struct RigidBody {
pub position: Vector3<f32>,
pub velocity: Vector3<f32>,
pub mass: f32,
pub inv_mass: f32,
pub body_type: BodyType,
pub restitution: f32,
pub collider: Option<Collider>,
pub friction: f32,
force_accumulator: Vector3<f32>,
pub damping: f32,
pub orientation: UnitQuaternion<f32>,
pub angular_velocity: Vector3<f32>,
pub inv_inertia_local: Matrix3<f32>,
torque_accumulator: Vector3<f32>,
pub angular_damping: f32,
pub active: bool,
}
impl RigidBody {
pub fn new(mass: f32) -> Self {
assert!(
mass > 0.0 && mass.is_finite(),
"mass must be positive and finite"
);
let i = 0.4 * mass;
let inv_i = 1.0 / i;
Self {
position: Vector3::zeros(),
velocity: Vector3::zeros(),
mass,
inv_mass: 1.0 / mass,
body_type: BodyType::Dynamic,
restitution: 0.5,
collider: None,
friction: 0.3,
force_accumulator: Vector3::zeros(),
damping: 0.01,
orientation: UnitQuaternion::identity(),
angular_velocity: Vector3::zeros(),
inv_inertia_local: Matrix3::from_diagonal(&Vector3::new(inv_i, inv_i, inv_i)),
torque_accumulator: Vector3::zeros(),
angular_damping: 0.01,
active: true,
}
}
pub fn new_static() -> Self {
Self {
position: Vector3::zeros(),
velocity: Vector3::zeros(),
mass: f32::INFINITY,
inv_mass: 0.0,
body_type: BodyType::Static,
restitution: 0.5,
collider: None,
friction: 0.5,
force_accumulator: Vector3::zeros(),
damping: 0.0,
orientation: UnitQuaternion::identity(),
angular_velocity: Vector3::zeros(),
inv_inertia_local: Matrix3::zeros(),
torque_accumulator: Vector3::zeros(),
angular_damping: 0.0,
active: true,
}
}
pub fn with_position(mut self, position: Vector3<f32>) -> Self {
self.position = position;
self
}
pub fn with_velocity(mut self, velocity: Vector3<f32>) -> Self {
self.velocity = velocity;
self
}
pub fn with_restitution(mut self, restitution: f32) -> Self {
self.restitution = restitution.clamp(0.0, 1.0);
self
}
pub fn with_damping(mut self, damping: f32) -> Self {
self.damping = damping.clamp(0.0, 1.0);
self
}
pub fn with_collider(mut self, collider: Collider) -> Self {
self.collider = Some(collider);
self
}
pub fn with_friction(mut self, friction: f32) -> Self {
self.friction = friction.clamp(0.0, 1.0);
self
}
pub fn with_angular_velocity(mut self, angular_velocity: Vector3<f32>) -> Self {
self.angular_velocity = angular_velocity;
self
}
pub fn with_angular_damping(mut self, damping: f32) -> Self {
self.angular_damping = damping.clamp(0.0, 1.0);
self
}
pub fn with_inertia_sphere(mut self, radius: f32) -> Self {
if self.body_type == BodyType::Dynamic {
let i = 0.4 * self.mass * radius * radius;
let inv_i = 1.0 / i;
self.inv_inertia_local = Matrix3::from_diagonal(&Vector3::new(inv_i, inv_i, inv_i));
}
self
}
pub fn with_inertia_box(mut self, half_extents: Vector3<f32>) -> Self {
if self.body_type == BodyType::Dynamic {
let hx2 = 4.0 * half_extents.x * half_extents.x;
let hy2 = 4.0 * half_extents.y * half_extents.y;
let hz2 = 4.0 * half_extents.z * half_extents.z;
let k = self.mass / 12.0;
let ix = k * (hy2 + hz2);
let iy = k * (hx2 + hz2);
let iz = k * (hx2 + hy2);
self.inv_inertia_local =
Matrix3::from_diagonal(&Vector3::new(1.0 / ix, 1.0 / iy, 1.0 / iz));
}
self
}
#[inline]
pub fn apply_force(&mut self, force: Vector3<f32>) {
self.force_accumulator += force;
}
#[inline]
pub fn apply_impulse(&mut self, impulse: Vector3<f32>) {
if self.body_type == BodyType::Dynamic {
self.velocity += impulse * self.inv_mass;
}
}
#[inline]
pub fn apply_torque(&mut self, torque: Vector3<f32>) {
self.torque_accumulator += torque;
}
#[inline]
pub fn apply_angular_impulse(&mut self, impulse: Vector3<f32>) {
if self.body_type == BodyType::Dynamic {
let inv_inertia_world = self.inv_inertia_world();
self.angular_velocity += inv_inertia_world * impulse;
}
}
#[inline]
pub fn inv_inertia_world(&self) -> Matrix3<f32> {
let r = self.orientation.to_rotation_matrix();
r.matrix() * self.inv_inertia_local * r.matrix().transpose()
}
#[inline]
pub fn speed(&self) -> f32 {
self.velocity.norm()
}
#[inline]
pub fn kinetic_energy(&self) -> f32 {
0.5 * self.mass * self.velocity.norm_squared()
}
fn integrate(&mut self, dt: f32, gravity: Vector3<f32>) {
if self.body_type != BodyType::Dynamic {
self.force_accumulator = Vector3::zeros();
self.torque_accumulator = Vector3::zeros();
return;
}
let acceleration = gravity + self.force_accumulator * self.inv_mass;
self.velocity += acceleration * dt;
self.velocity *= 1.0 - self.damping;
self.position += self.velocity * dt;
let inv_inertia_world = self.inv_inertia_world();
let angular_acceleration = inv_inertia_world * self.torque_accumulator;
self.angular_velocity += angular_acceleration * dt;
self.angular_velocity *= 1.0 - self.angular_damping;
let w = &self.angular_velocity;
let half_dt = 0.5 * dt;
let dq = nalgebra::Quaternion::new(0.0, w.x * half_dt, w.y * half_dt, w.z * half_dt);
let q = self.orientation.into_inner();
let new_q = q + dq * q;
self.orientation = UnitQuaternion::new_normalize(new_q);
self.force_accumulator = Vector3::zeros();
self.torque_accumulator = Vector3::zeros();
}
}
fn collide(
pos_a: &Vector3<f32>,
col_a: &Collider,
pos_b: &Vector3<f32>,
col_b: &Collider,
) -> Option<(Vector3<f32>, f32)> {
match (col_a, col_b) {
(Collider::Sphere { radius: ra }, Collider::Sphere { radius: rb }) => {
collide_sphere_sphere(pos_a, *ra, pos_b, *rb)
}
(Collider::Aabb { half_extents: ha }, Collider::Aabb { half_extents: hb }) => {
collide_aabb_aabb(pos_a, ha, pos_b, hb)
}
(Collider::Sphere { radius }, Collider::Aabb { half_extents }) => {
collide_sphere_aabb(pos_a, *radius, pos_b, half_extents)
}
(Collider::Aabb { half_extents }, Collider::Sphere { radius }) => {
let result = collide_sphere_aabb(pos_b, *radius, pos_a, half_extents);
result.map(|(normal, pen)| (-normal, pen))
}
(
Collider::Capsule {
height: ha,
radius: ra,
},
Collider::Capsule {
height: hb,
radius: rb,
},
) => collide_capsule_capsule(pos_a, *ha, *ra, pos_b, *hb, *rb),
(
Collider::Sphere { radius },
Collider::Capsule {
height,
radius: cap_radius,
},
) => collide_sphere_capsule(pos_a, *radius, pos_b, *height, *cap_radius),
(
Collider::Capsule {
height,
radius: cap_radius,
},
Collider::Sphere { radius },
) => {
let result = collide_sphere_capsule(pos_b, *radius, pos_a, *height, *cap_radius);
result.map(|(normal, pen)| (-normal, pen))
}
(Collider::Capsule { height, radius }, Collider::Aabb { half_extents }) => {
collide_capsule_aabb(pos_a, *height, *radius, pos_b, half_extents)
}
(Collider::Aabb { half_extents }, Collider::Capsule { height, radius }) => {
let result = collide_capsule_aabb(pos_b, *height, *radius, pos_a, half_extents);
result.map(|(normal, pen)| (-normal, pen))
}
}
}
fn collide_sphere_sphere(
pos_a: &Vector3<f32>,
radius_a: f32,
pos_b: &Vector3<f32>,
radius_b: f32,
) -> Option<(Vector3<f32>, f32)> {
let diff = pos_b - pos_a;
let dist_sq = diff.norm_squared();
let sum_r = radius_a + radius_b;
if dist_sq >= sum_r * sum_r {
return None;
}
let dist = dist_sq.sqrt();
let penetration = sum_r - dist;
let normal = if dist > 1e-6 {
diff / dist
} else {
Vector3::new(0.0, 1.0, 0.0)
};
Some((normal, penetration))
}
fn collide_aabb_aabb(
pos_a: &Vector3<f32>,
half_a: &Vector3<f32>,
pos_b: &Vector3<f32>,
half_b: &Vector3<f32>,
) -> Option<(Vector3<f32>, f32)> {
let diff = pos_b - pos_a;
let overlap_x = half_a.x + half_b.x - diff.x.abs();
if overlap_x <= 0.0 {
return None;
}
let overlap_y = half_a.y + half_b.y - diff.y.abs();
if overlap_y <= 0.0 {
return None;
}
let overlap_z = half_a.z + half_b.z - diff.z.abs();
if overlap_z <= 0.0 {
return None;
}
if overlap_x <= overlap_y && overlap_x <= overlap_z {
let sign = if diff.x >= 0.0 { 1.0 } else { -1.0 };
Some((Vector3::new(sign, 0.0, 0.0), overlap_x))
} else if overlap_y <= overlap_z {
let sign = if diff.y >= 0.0 { 1.0 } else { -1.0 };
Some((Vector3::new(0.0, sign, 0.0), overlap_y))
} else {
let sign = if diff.z >= 0.0 { 1.0 } else { -1.0 };
Some((Vector3::new(0.0, 0.0, sign), overlap_z))
}
}
fn collide_sphere_aabb(
sphere_pos: &Vector3<f32>,
sphere_radius: f32,
aabb_pos: &Vector3<f32>,
aabb_half: &Vector3<f32>,
) -> Option<(Vector3<f32>, f32)> {
let aabb_min = aabb_pos - aabb_half;
let aabb_max = aabb_pos + aabb_half;
let closest = Vector3::new(
sphere_pos.x.clamp(aabb_min.x, aabb_max.x),
sphere_pos.y.clamp(aabb_min.y, aabb_max.y),
sphere_pos.z.clamp(aabb_min.z, aabb_max.z),
);
let diff = sphere_pos - closest;
let dist_sq = diff.norm_squared();
if dist_sq >= sphere_radius * sphere_radius {
return None;
}
let dist = dist_sq.sqrt();
if dist > 1e-6 {
let normal = -diff / dist;
let penetration = sphere_radius - dist;
Some((normal, penetration))
} else {
let dx_pos = aabb_max.x - sphere_pos.x;
let dx_neg = sphere_pos.x - aabb_min.x;
let dy_pos = aabb_max.y - sphere_pos.y;
let dy_neg = sphere_pos.y - aabb_min.y;
let dz_pos = aabb_max.z - sphere_pos.z;
let dz_neg = sphere_pos.z - aabb_min.z;
let mut min_dist = dx_pos;
let mut normal = Vector3::new(1.0, 0.0, 0.0);
if dx_neg < min_dist {
min_dist = dx_neg;
normal = Vector3::new(-1.0, 0.0, 0.0);
}
if dy_pos < min_dist {
min_dist = dy_pos;
normal = Vector3::new(0.0, 1.0, 0.0);
}
if dy_neg < min_dist {
min_dist = dy_neg;
normal = Vector3::new(0.0, -1.0, 0.0);
}
if dz_pos < min_dist {
min_dist = dz_pos;
normal = Vector3::new(0.0, 0.0, 1.0);
}
if dz_neg < min_dist {
min_dist = dz_neg;
normal = Vector3::new(0.0, 0.0, -1.0);
}
let penetration = sphere_radius + min_dist;
Some((normal, penetration))
}
}
fn collide_capsule_capsule(
pos_a: &Vector3<f32>,
height_a: f32,
radius_a: f32,
pos_b: &Vector3<f32>,
height_b: f32,
radius_b: f32,
) -> Option<(Vector3<f32>, f32)> {
let half_height_a = height_a * 0.5;
let half_height_b = height_b * 0.5;
let a_bottom = *pos_a + Vector3::new(0.0, -half_height_a, 0.0);
let a_top = *pos_a + Vector3::new(0.0, half_height_a, 0.0);
let b_bottom = *pos_b + Vector3::new(0.0, -half_height_b, 0.0);
let b_top = *pos_b + Vector3::new(0.0, half_height_b, 0.0);
let (closest_a, closest_b) = closest_points_on_segments(a_bottom, a_top, b_bottom, b_top);
collide_sphere_sphere(&closest_a, radius_a, &closest_b, radius_b)
}
fn collide_sphere_capsule(
sphere_pos: &Vector3<f32>,
sphere_radius: f32,
capsule_pos: &Vector3<f32>,
capsule_height: f32,
capsule_radius: f32,
) -> Option<(Vector3<f32>, f32)> {
let half_height = capsule_height * 0.5;
let cap_bottom = *capsule_pos + Vector3::new(0.0, -half_height, 0.0);
let cap_top = *capsule_pos + Vector3::new(0.0, half_height, 0.0);
let closest = closest_point_on_segment(*sphere_pos, cap_bottom, cap_top);
collide_sphere_sphere(sphere_pos, sphere_radius, &closest, capsule_radius)
}
fn collide_capsule_aabb(
capsule_pos: &Vector3<f32>,
capsule_height: f32,
capsule_radius: f32,
aabb_pos: &Vector3<f32>,
aabb_half_extents: &Vector3<f32>,
) -> Option<(Vector3<f32>, f32)> {
let half_height = capsule_height * 0.5;
let cap_bottom = *capsule_pos + Vector3::new(0.0, -half_height, 0.0);
let cap_top = *capsule_pos + Vector3::new(0.0, half_height, 0.0);
let aabb_min = aabb_pos - aabb_half_extents;
let aabb_max = aabb_pos + aabb_half_extents;
let closest_on_capsule =
closest_point_on_segment_to_aabb(cap_bottom, cap_top, &aabb_min, &aabb_max);
collide_sphere_aabb(
&closest_on_capsule,
capsule_radius,
aabb_pos,
aabb_half_extents,
)
}
fn closest_point_on_segment(
point: Vector3<f32>,
seg_start: Vector3<f32>,
seg_end: Vector3<f32>,
) -> Vector3<f32> {
let segment = seg_end - seg_start;
let segment_len_sq = segment.norm_squared();
if segment_len_sq < 1e-6 {
return seg_start;
}
let t = ((point - seg_start).dot(&segment) / segment_len_sq).clamp(0.0, 1.0);
seg_start + segment * t
}
fn closest_points_on_segments(
a_start: Vector3<f32>,
a_end: Vector3<f32>,
b_start: Vector3<f32>,
b_end: Vector3<f32>,
) -> (Vector3<f32>, Vector3<f32>) {
let d1 = a_end - a_start;
let d2 = b_end - b_start;
let r = a_start - b_start;
let a = d1.dot(&d1);
let e = d2.dot(&d2);
let f = d2.dot(&r);
let epsilon = 1e-6;
if a < epsilon && e < epsilon {
return (a_start, b_start);
}
let mut s: f32;
let mut t: f32;
if a < epsilon {
s = 0.0;
t = (f / e).clamp(0.0, 1.0);
} else {
let c = d1.dot(&r);
if e < epsilon {
t = 0.0;
s = (-c / a).clamp(0.0, 1.0);
} else {
let b = d1.dot(&d2);
let denom = a * e - b * b;
s = if denom.abs() > epsilon {
((b * f - c * e) / denom).clamp(0.0, 1.0)
} else {
0.0
};
t = (b * s + f) / e;
if t < 0.0 {
t = 0.0;
s = (-c / a).clamp(0.0, 1.0);
} else if t > 1.0 {
t = 1.0;
s = ((b - c) / a).clamp(0.0, 1.0);
}
}
}
(a_start + d1 * s, b_start + d2 * t)
}
fn closest_point_on_segment_to_aabb(
seg_start: Vector3<f32>,
seg_end: Vector3<f32>,
aabb_min: &Vector3<f32>,
aabb_max: &Vector3<f32>,
) -> Vector3<f32> {
let aabb_center = (aabb_min + aabb_max) * 0.5;
closest_point_on_segment(aabb_center, seg_start, seg_end)
}
#[derive(Debug, Clone, Copy)]
pub struct Ray {
pub origin: Vector3<f32>,
pub direction: Vector3<f32>,
}
impl Ray {
pub fn new(origin: Vector3<f32>, direction: Vector3<f32>) -> Self {
Self {
origin,
direction: direction.normalize(),
}
}
pub fn point_at(&self, t: f32) -> Vector3<f32> {
self.origin + self.direction * t
}
}
#[derive(Debug, Clone, Copy)]
pub struct RayCastHit {
pub body_id: BodyId,
pub point: Vector3<f32>,
pub normal: Vector3<f32>,
pub distance: f32,
}
fn ray_intersect_sphere(ray: &Ray, center: &Vector3<f32>, radius: f32) -> Option<f32> {
let oc = ray.origin - center;
let a = ray.direction.dot(&ray.direction);
let b = 2.0 * oc.dot(&ray.direction);
let c = oc.dot(&oc) - radius * radius;
let discriminant = b * b - 4.0 * a * c;
if discriminant < 0.0 {
return None;
}
let sqrt_disc = discriminant.sqrt();
let t1 = (-b - sqrt_disc) / (2.0 * a);
let t2 = (-b + sqrt_disc) / (2.0 * a);
if t1 > 0.0 {
Some(t1)
} else if t2 > 0.0 {
Some(t2)
} else {
None
}
}
fn ray_intersect_aabb(
ray: &Ray,
aabb_pos: &Vector3<f32>,
half_extents: &Vector3<f32>,
) -> Option<f32> {
let aabb_min = aabb_pos - half_extents;
let aabb_max = aabb_pos + half_extents;
let mut tmin = (aabb_min.x - ray.origin.x) / ray.direction.x;
let mut tmax = (aabb_max.x - ray.origin.x) / ray.direction.x;
if tmin > tmax {
core::mem::swap(&mut tmin, &mut tmax);
}
let mut tymin = (aabb_min.y - ray.origin.y) / ray.direction.y;
let mut tymax = (aabb_max.y - ray.origin.y) / ray.direction.y;
if tymin > tymax {
core::mem::swap(&mut tymin, &mut tymax);
}
if (tmin > tymax) || (tymin > tmax) {
return None;
}
tmin = tmin.max(tymin);
tmax = tmax.min(tymax);
let mut tzmin = (aabb_min.z - ray.origin.z) / ray.direction.z;
let mut tzmax = (aabb_max.z - ray.origin.z) / ray.direction.z;
if tzmin > tzmax {
core::mem::swap(&mut tzmin, &mut tzmax);
}
if (tmin > tzmax) || (tzmin > tmax) {
return None;
}
tmin = tmin.max(tzmin);
if tmin > 0.0 {
Some(tmin)
} else if tmax > 0.0 {
Some(tmax)
} else {
None
}
}
fn ray_intersect_capsule(
ray: &Ray,
capsule_pos: &Vector3<f32>,
height: f32,
radius: f32,
) -> Option<f32> {
let half_height = height * 0.5;
let cap_bottom = *capsule_pos + Vector3::new(0.0, -half_height, 0.0);
let cap_top = *capsule_pos + Vector3::new(0.0, half_height, 0.0);
let mut min_t = f32::MAX;
let mut hit = false;
if let Some(t) = ray_intersect_sphere(ray, &cap_bottom, radius) {
min_t = min_t.min(t);
hit = true;
}
if let Some(t) = ray_intersect_sphere(ray, &cap_top, radius) {
min_t = min_t.min(t);
hit = true;
}
if hit { Some(min_t) } else { None }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ConstraintId(usize);
#[derive(Debug, Clone)]
pub enum Constraint {
Distance {
body_a: BodyId,
body_b: BodyId,
anchor_a: Vector3<f32>,
anchor_b: Vector3<f32>,
rest_length: f32,
compliance: f32,
},
BallSocket {
body_a: BodyId,
body_b: BodyId,
anchor_a: Vector3<f32>,
anchor_b: Vector3<f32>,
compliance: f32,
},
Fixed {
body_a: BodyId,
body_b: BodyId,
anchor_a: Vector3<f32>,
anchor_b: Vector3<f32>,
relative_orientation: UnitQuaternion<f32>,
compliance: f32,
},
}
pub struct PhysicsWorld<const N: usize, const M: usize = 0> {
bodies: heapless::Vec<RigidBody, N>,
constraints: heapless::Vec<Constraint, M>,
gravity: Vector3<f32>,
pub solver_iterations: u32,
}
impl<const N: usize, const M: usize> PhysicsWorld<N, M> {
pub fn new() -> Self {
Self {
bodies: heapless::Vec::new(),
constraints: heapless::Vec::new(),
gravity: Vector3::zeros(),
solver_iterations: 4,
}
}
pub fn set_gravity(&mut self, gravity: Vector3<f32>) {
self.gravity = gravity;
}
pub fn gravity(&self) -> Vector3<f32> {
self.gravity
}
pub fn add_body(&mut self, body: RigidBody) -> Option<BodyId> {
let id = BodyId(self.bodies.len());
self.bodies.push(body).ok()?;
Some(id)
}
pub fn body(&self, id: BodyId) -> Option<&RigidBody> {
self.bodies.get(id.0)
}
pub fn body_mut(&mut self, id: BodyId) -> Option<&mut RigidBody> {
self.bodies.get_mut(id.0)
}
pub fn body_count(&self) -> usize {
self.bodies.len()
}
pub fn active_body_count(&self) -> usize {
self.bodies.iter().filter(|b| b.active).count()
}
pub fn remove_body(&mut self, id: BodyId) -> bool {
if let Some(body) = self.bodies.get_mut(id.0) {
if body.active {
body.active = false;
body.velocity = Vector3::zeros();
body.angular_velocity = Vector3::zeros();
body.force_accumulator = Vector3::zeros();
body.torque_accumulator = Vector3::zeros();
return true;
}
}
false
}
pub fn set_active(&mut self, id: BodyId, active: bool) -> bool {
if let Some(body) = self.bodies.get_mut(id.0) {
body.active = active;
true
} else {
false
}
}
pub fn bodies(&self) -> impl Iterator<Item = (BodyId, &RigidBody)> {
self.bodies.iter().enumerate().map(|(i, b)| (BodyId(i), b))
}
pub fn bodies_mut(&mut self) -> impl Iterator<Item = (BodyId, &mut RigidBody)> {
self.bodies
.iter_mut()
.enumerate()
.map(|(i, b)| (BodyId(i), b))
}
pub fn ray_cast(&self, ray: &Ray, max_distance: f32) -> Option<RayCastHit> {
let mut nearest_hit: Option<RayCastHit> = None;
let mut min_distance = max_distance;
for (i, body) in self.bodies.iter().enumerate() {
if !body.active {
continue;
}
let collider = match &body.collider {
Some(c) => c,
None => continue,
};
let distance = match collider {
Collider::Sphere { radius } => ray_intersect_sphere(ray, &body.position, *radius),
Collider::Aabb { half_extents } => {
ray_intersect_aabb(ray, &body.position, half_extents)
}
Collider::Capsule { height, radius } => {
ray_intersect_capsule(ray, &body.position, *height, *radius)
}
};
if let Some(dist) = distance {
if dist < min_distance {
let point = ray.point_at(dist);
let normal = match collider {
Collider::Sphere { .. } => (point - body.position).normalize(),
Collider::Aabb { .. } => {
(point - body.position).normalize()
}
Collider::Capsule { height, .. } => {
let half_height = height * 0.5;
let cap_bottom = body.position + Vector3::new(0.0, -half_height, 0.0);
let cap_top = body.position + Vector3::new(0.0, half_height, 0.0);
let closest = closest_point_on_segment(point, cap_bottom, cap_top);
(point - closest).normalize()
}
};
min_distance = dist;
nearest_hit = Some(RayCastHit {
body_id: BodyId(i),
point,
normal,
distance: dist,
});
}
}
}
nearest_hit
}
pub fn add_constraint(&mut self, constraint: Constraint) -> Option<ConstraintId> {
let id = ConstraintId(self.constraints.len());
self.constraints.push(constraint).ok()?;
Some(id)
}
pub fn constraint(&self, id: ConstraintId) -> Option<&Constraint> {
self.constraints.get(id.0)
}
pub fn constraint_mut(&mut self, id: ConstraintId) -> Option<&mut Constraint> {
self.constraints.get_mut(id.0)
}
pub fn remove_constraint(&mut self, id: ConstraintId) -> bool {
if id.0 < self.constraints.len() {
self.constraints.swap_remove(id.0);
true
} else {
false
}
}
pub fn constraint_count(&self) -> usize {
self.constraints.len()
}
pub fn add_distance_constraint(
&mut self,
body_a: BodyId,
anchor_a: Vector3<f32>,
body_b: BodyId,
anchor_b: Vector3<f32>,
compliance: f32,
) -> Option<ConstraintId> {
let world_a = self.body_to_world(body_a, &anchor_a)?;
let world_b = self.body_to_world(body_b, &anchor_b)?;
let rest_length = (world_b - world_a).norm();
self.add_constraint(Constraint::Distance {
body_a,
body_b,
anchor_a,
anchor_b,
rest_length,
compliance,
})
}
pub fn add_ball_socket(
&mut self,
body_a: BodyId,
anchor_a: Vector3<f32>,
body_b: BodyId,
anchor_b: Vector3<f32>,
compliance: f32,
) -> Option<ConstraintId> {
self.add_constraint(Constraint::BallSocket {
body_a,
body_b,
anchor_a,
anchor_b,
compliance,
})
}
pub fn add_fixed_joint(
&mut self,
body_a: BodyId,
anchor_a: Vector3<f32>,
body_b: BodyId,
anchor_b: Vector3<f32>,
compliance: f32,
) -> Option<ConstraintId> {
let qa = self.bodies.get(body_a.0)?.orientation;
let qb = self.bodies.get(body_b.0)?.orientation;
let relative_orientation = qb * qa.inverse();
self.add_constraint(Constraint::Fixed {
body_a,
body_b,
anchor_a,
anchor_b,
relative_orientation,
compliance,
})
}
fn body_to_world(&self, id: BodyId, local_point: &Vector3<f32>) -> Option<Vector3<f32>> {
let body = self.bodies.get(id.0)?;
Some(body.position + body.orientation * local_point)
}
#[inline]
fn body_x_extent(body: &RigidBody, collider: &Collider) -> (f32, f32) {
let px = body.position.x;
match collider {
Collider::Sphere { radius } => (px - radius, px + radius),
Collider::Aabb { half_extents } => (px - half_extents.x, px + half_extents.x),
Collider::Capsule { radius, .. } => (px - radius, px + radius),
}
}
pub fn detect_collisions<const C: usize>(&self) -> heapless::Vec<Contact, C> {
let mut contacts = heapless::Vec::new();
let mut entries = heapless::Vec::<(f32, f32, usize), N>::new();
for (i, body) in self.bodies.iter().enumerate() {
if !body.active {
continue;
}
if let Some(ref col) = body.collider {
let (min_x, max_x) = Self::body_x_extent(body, col);
let _ = entries.push((min_x, max_x, i));
}
}
for i in 1..entries.len() {
let mut j = i;
while j > 0 && entries[j].0 < entries[j - 1].0 {
entries.swap(j, j - 1);
j -= 1;
}
}
let len = entries.len();
for i in 0..len {
let (_, max_x_i, idx_a) = entries[i];
let body_a = &self.bodies[idx_a];
let col_a = body_a.collider.as_ref().unwrap();
for j in (i + 1)..len {
let (min_x_j, _, idx_b) = entries[j];
if min_x_j > max_x_i {
break;
}
let body_b = &self.bodies[idx_b];
let col_b = body_b.collider.as_ref().unwrap();
if body_a.body_type == BodyType::Static && body_b.body_type == BodyType::Static {
continue;
}
if let Some((normal, penetration)) =
collide(&body_a.position, col_a, &body_b.position, col_b)
{
let _ = contacts.push(Contact {
body_a: BodyId(idx_a),
body_b: BodyId(idx_b),
normal,
penetration,
});
}
}
}
contacts
}
pub fn resolve_contacts(&mut self, contacts: &[Contact]) {
for contact in contacts {
let a = contact.body_a.0;
let b = contact.body_b.0;
let inv_mass_a = self.bodies[a].inv_mass;
let inv_mass_b = self.bodies[b].inv_mass;
let contact_point =
self.bodies[a].position + contact.normal * (contact.penetration * 0.5);
let ra = contact_point - self.bodies[a].position;
let rb = contact_point - self.bodies[b].position;
let inv_inertia_a = self.bodies[a].inv_inertia_world();
let inv_inertia_b = self.bodies[b].inv_inertia_world();
let inv_mass_sum = inv_mass_a + inv_mass_b;
if inv_mass_sum == 0.0 {
continue; }
let correction = contact.normal * (contact.penetration / inv_mass_sum);
self.bodies[a].position -= correction * inv_mass_a;
self.bodies[b].position += correction * inv_mass_b;
let vel_a = self.bodies[a].velocity + self.bodies[a].angular_velocity.cross(&ra);
let vel_b = self.bodies[b].velocity + self.bodies[b].angular_velocity.cross(&rb);
let relative_vel = vel_b - vel_a;
let vel_along_normal = relative_vel.dot(&contact.normal);
if vel_along_normal >= 0.0 {
continue;
}
let restitution = self.bodies[a].restitution.min(self.bodies[b].restitution);
let ra_cross_n = ra.cross(&contact.normal);
let rb_cross_n = rb.cross(&contact.normal);
let angular_term_a = (inv_inertia_a * ra_cross_n).cross(&ra).dot(&contact.normal);
let angular_term_b = (inv_inertia_b * rb_cross_n).cross(&rb).dot(&contact.normal);
let eff_mass_inv = inv_mass_a + inv_mass_b + angular_term_a + angular_term_b;
if eff_mass_inv <= 0.0 {
continue;
}
let j = -(1.0 + restitution) * vel_along_normal / eff_mass_inv;
let impulse = contact.normal * j;
self.bodies[a].velocity -= impulse * inv_mass_a;
self.bodies[b].velocity += impulse * inv_mass_b;
self.bodies[a].angular_velocity -= inv_inertia_a * ra.cross(&impulse);
self.bodies[b].angular_velocity += inv_inertia_b * rb.cross(&impulse);
let mu_a = self.bodies[a].friction;
let mu_b = self.bodies[b].friction;
let mu = (mu_a * mu_b).sqrt();
if mu > 1e-6 {
let vel_a = self.bodies[a].velocity + self.bodies[a].angular_velocity.cross(&ra);
let vel_b = self.bodies[b].velocity + self.bodies[b].angular_velocity.cross(&rb);
let relative_vel = vel_b - vel_a;
let vn = relative_vel.dot(&contact.normal);
let tangent_vel = relative_vel - contact.normal * vn;
let tangent_speed = tangent_vel.norm();
if tangent_speed > 1e-6 {
let tangent = tangent_vel / tangent_speed;
let ra_cross_t = ra.cross(&tangent);
let rb_cross_t = rb.cross(&tangent);
let ang_t_a = (inv_inertia_a * ra_cross_t).cross(&ra).dot(&tangent);
let ang_t_b = (inv_inertia_b * rb_cross_t).cross(&rb).dot(&tangent);
let eff_mass_t_inv = inv_mass_a + inv_mass_b + ang_t_a + ang_t_b;
if eff_mass_t_inv > 0.0 {
let jt = (-tangent_speed / eff_mass_t_inv).max(-mu * j);
let friction_impulse = tangent * jt;
self.bodies[a].velocity -= friction_impulse * inv_mass_a;
self.bodies[b].velocity += friction_impulse * inv_mass_b;
self.bodies[a].angular_velocity -=
inv_inertia_a * ra.cross(&friction_impulse);
self.bodies[b].angular_velocity +=
inv_inertia_b * rb.cross(&friction_impulse);
}
}
}
}
}
pub fn solve_constraints(&mut self, dt: f32) {
if self.constraints.is_empty() || dt <= 0.0 {
return;
}
let iterations = self.solver_iterations;
for _ in 0..iterations {
for ci in 0..self.constraints.len() {
let constraint = self.constraints[ci].clone();
match &constraint {
Constraint::Distance {
body_a,
body_b,
anchor_a,
anchor_b,
rest_length,
compliance,
} => {
self.solve_distance(
body_a.0,
anchor_a,
body_b.0,
anchor_b,
*rest_length,
*compliance,
dt,
);
}
Constraint::BallSocket {
body_a,
body_b,
anchor_a,
anchor_b,
compliance,
} => {
self.solve_distance(
body_a.0,
anchor_a,
body_b.0,
anchor_b,
0.0,
*compliance,
dt,
);
}
Constraint::Fixed {
body_a,
body_b,
anchor_a,
anchor_b,
relative_orientation,
compliance,
} => {
self.solve_distance(
body_a.0,
anchor_a,
body_b.0,
anchor_b,
0.0,
*compliance,
dt,
);
self.solve_orientation(
body_a.0,
body_b.0,
relative_orientation,
*compliance,
dt,
);
}
}
}
}
}
fn solve_distance(
&mut self,
a: usize,
anchor_a: &Vector3<f32>,
b: usize,
anchor_b: &Vector3<f32>,
rest_length: f32,
compliance: f32,
dt: f32,
) {
if !self.bodies[a].active && !self.bodies[b].active {
return;
}
let ra = self.bodies[a].orientation * anchor_a;
let rb = self.bodies[b].orientation * anchor_b;
let world_a = self.bodies[a].position + ra;
let world_b = self.bodies[b].position + rb;
let diff = world_b - world_a;
let dist = diff.norm();
let error = dist - rest_length;
if error.abs() < 1e-6 {
return;
}
let n = if dist > 1e-6 {
diff / dist
} else {
Vector3::new(0.0, 1.0, 0.0) };
let inv_mass_a = self.bodies[a].inv_mass;
let inv_mass_b = self.bodies[b].inv_mass;
let inv_inertia_a = self.bodies[a].inv_inertia_world();
let inv_inertia_b = self.bodies[b].inv_inertia_world();
let ra_cross_n = ra.cross(&n);
let rb_cross_n = rb.cross(&n);
let w_a = inv_mass_a + ra_cross_n.dot(&(inv_inertia_a * ra_cross_n));
let w_b = inv_mass_b + rb_cross_n.dot(&(inv_inertia_b * rb_cross_n));
let w_sum = w_a + w_b;
if w_sum <= 0.0 {
return;
}
let alpha = compliance / (dt * dt);
let delta_lambda = -error / (w_sum + alpha);
let correction = n * delta_lambda;
self.bodies[a].position -= correction * inv_mass_a;
self.bodies[b].position += correction * inv_mass_b;
self.bodies[a].angular_velocity -= inv_inertia_a * ra.cross(&correction) / dt;
self.bodies[b].angular_velocity += inv_inertia_b * rb.cross(&correction) / dt;
self.bodies[a].velocity -= correction * inv_mass_a / dt;
self.bodies[b].velocity += correction * inv_mass_b / dt;
}
fn solve_orientation(
&mut self,
a: usize,
b: usize,
target_rel: &UnitQuaternion<f32>,
compliance: f32,
dt: f32,
) {
if !self.bodies[a].active && !self.bodies[b].active {
return;
}
let qa = self.bodies[a].orientation;
let qb = self.bodies[b].orientation;
let current_rel = qb * qa.inverse();
let error_q = current_rel * target_rel.inverse();
let error_inner = error_q.into_inner();
let error_vec = Vector3::new(error_inner.i, error_inner.j, error_inner.k);
let error = if error_inner.w >= 0.0 {
error_vec * 2.0
} else {
-error_vec * 2.0
};
if error.norm_squared() < 1e-10 {
return;
}
let inv_inertia_a = self.bodies[a].inv_inertia_world();
let inv_inertia_b = self.bodies[b].inv_inertia_world();
let w_a = if self.bodies[a].body_type == BodyType::Dynamic {
inv_inertia_a[(0, 0)] + inv_inertia_a[(1, 1)] + inv_inertia_a[(2, 2)]
} else {
0.0
};
let w_b = if self.bodies[b].body_type == BodyType::Dynamic {
inv_inertia_b[(0, 0)] + inv_inertia_b[(1, 1)] + inv_inertia_b[(2, 2)]
} else {
0.0
};
let w_sum = w_a + w_b;
if w_sum <= 0.0 {
return;
}
let alpha = compliance / (dt * dt);
let delta_lambda = -error / (w_sum + alpha);
if self.bodies[a].body_type == BodyType::Dynamic {
self.bodies[a].angular_velocity -= inv_inertia_a * delta_lambda / dt;
}
if self.bodies[b].body_type == BodyType::Dynamic {
self.bodies[b].angular_velocity += inv_inertia_b * delta_lambda / dt;
}
}
pub fn step<const C: usize>(&mut self, dt: f32) {
let gravity = self.gravity;
for body in self.bodies.iter_mut() {
if body.active {
body.integrate(dt, gravity);
}
}
let contacts = self.detect_collisions::<C>();
self.resolve_contacts(&contacts);
self.solve_constraints(dt);
}
pub fn step_fixed<const C: usize>(&mut self, dt: f32, substeps: u32) {
let sub_dt = dt / substeps as f32;
for _ in 0..substeps {
self.step::<C>(sub_dt);
}
}
}
pub fn sync_body_to_mesh(body: &RigidBody, mesh: &mut crate::mesh::K3dMesh<'_>) {
mesh.set_position(body.position.x, body.position.y, body.position.z);
mesh.set_rotation(body.orientation);
}
#[cfg(test)]
mod tests {
extern crate std;
use super::*;
const EPSILON: f32 = 1e-4;
fn approx_eq(a: f32, b: f32) -> bool {
(a - b).abs() < EPSILON
}
fn approx_vec_eq(a: &Vector3<f32>, b: &Vector3<f32>) -> bool {
approx_eq(a.x, b.x) && approx_eq(a.y, b.y) && approx_eq(a.z, b.z)
}
#[test]
fn test_body_creation() {
let body = RigidBody::new(5.0);
assert_eq!(body.mass, 5.0);
assert!(approx_eq(body.inv_mass, 0.2));
assert_eq!(body.body_type, BodyType::Dynamic);
assert!(approx_vec_eq(&body.position, &Vector3::zeros()));
assert!(approx_vec_eq(&body.velocity, &Vector3::zeros()));
}
#[test]
fn test_static_body() {
let body = RigidBody::new_static();
assert_eq!(body.body_type, BodyType::Static);
assert_eq!(body.inv_mass, 0.0);
assert!(body.mass.is_infinite());
}
#[test]
#[should_panic]
fn test_body_zero_mass_panics() {
RigidBody::new(0.0);
}
#[test]
#[should_panic]
fn test_body_negative_mass_panics() {
RigidBody::new(-1.0);
}
#[test]
fn test_builder_pattern() {
let body = RigidBody::new(1.0)
.with_position(Vector3::new(1.0, 2.0, 3.0))
.with_velocity(Vector3::new(0.0, 5.0, 0.0))
.with_restitution(0.8)
.with_damping(0.05);
assert!(approx_vec_eq(&body.position, &Vector3::new(1.0, 2.0, 3.0)));
assert!(approx_vec_eq(&body.velocity, &Vector3::new(0.0, 5.0, 0.0)));
assert!(approx_eq(body.restitution, 0.8));
assert!(approx_eq(body.damping, 0.05));
}
#[test]
fn test_apply_force() {
let mut body = RigidBody::new(1.0);
body.apply_force(Vector3::new(10.0, 0.0, 0.0));
body.apply_force(Vector3::new(0.0, 5.0, 0.0));
assert!(approx_vec_eq(
&body.force_accumulator,
&Vector3::new(10.0, 5.0, 0.0)
));
}
#[test]
fn test_apply_impulse() {
let mut body = RigidBody::new(2.0);
body.apply_impulse(Vector3::new(10.0, 0.0, 0.0));
assert!(approx_vec_eq(&body.velocity, &Vector3::new(5.0, 0.0, 0.0)));
}
#[test]
fn test_impulse_on_static_body_ignored() {
let mut body = RigidBody::new_static();
body.apply_impulse(Vector3::new(100.0, 0.0, 0.0));
assert!(approx_vec_eq(&body.velocity, &Vector3::zeros()));
}
#[test]
fn test_speed() {
let body = RigidBody::new(1.0).with_velocity(Vector3::new(3.0, 4.0, 0.0));
assert!(approx_eq(body.speed(), 5.0));
}
#[test]
fn test_kinetic_energy() {
let body = RigidBody::new(2.0).with_velocity(Vector3::new(3.0, 0.0, 0.0));
assert!(approx_eq(body.kinetic_energy(), 9.0));
}
#[test]
fn test_world_creation() {
let world = PhysicsWorld::<8>::new();
assert_eq!(world.body_count(), 0);
assert!(approx_vec_eq(&world.gravity(), &Vector3::zeros()));
}
#[test]
fn test_add_and_get_body() {
let mut world = PhysicsWorld::<8>::new();
let body = RigidBody::new(1.0).with_position(Vector3::new(1.0, 2.0, 3.0));
let id = world.add_body(body).unwrap();
assert_eq!(world.body_count(), 1);
let b = world.body(id).unwrap();
assert!(approx_vec_eq(&b.position, &Vector3::new(1.0, 2.0, 3.0)));
}
#[test]
fn test_add_body_at_capacity() {
let mut world = PhysicsWorld::<2>::new();
assert!(world.add_body(RigidBody::new(1.0)).is_some());
assert!(world.add_body(RigidBody::new(1.0)).is_some());
assert!(world.add_body(RigidBody::new(1.0)).is_none()); }
#[test]
fn test_gravity_freefall() {
let mut world = PhysicsWorld::<4>::new();
world.set_gravity(Vector3::new(0.0, -10.0, 0.0));
let body = RigidBody::new(1.0)
.with_position(Vector3::new(0.0, 100.0, 0.0))
.with_damping(0.0);
let id = world.add_body(body).unwrap();
world.step::<4>(1.0);
let b = world.body(id).unwrap();
assert!(approx_eq(b.velocity.y, -10.0));
assert!(approx_eq(b.position.y, 90.0));
}
#[test]
fn test_static_body_not_affected_by_gravity() {
let mut world = PhysicsWorld::<4>::new();
world.set_gravity(Vector3::new(0.0, -9.81, 0.0));
let body = RigidBody::new_static().with_position(Vector3::new(0.0, 0.0, 0.0));
let id = world.add_body(body).unwrap();
world.step::<4>(1.0);
let b = world.body(id).unwrap();
assert!(approx_vec_eq(&b.position, &Vector3::zeros()));
assert!(approx_vec_eq(&b.velocity, &Vector3::zeros()));
}
#[test]
fn test_force_accumulation_and_clearing() {
let mut world = PhysicsWorld::<4>::new();
let body = RigidBody::new(1.0).with_damping(0.0);
let id = world.add_body(body).unwrap();
world
.body_mut(id)
.unwrap()
.apply_force(Vector3::new(10.0, 0.0, 0.0));
world.step::<4>(1.0);
let b = world.body(id).unwrap();
assert!(approx_eq(b.velocity.x, 10.0));
world.step::<4>(1.0);
let b = world.body(id).unwrap();
assert!(approx_eq(b.velocity.x, 10.0)); }
#[test]
fn test_damping_reduces_velocity() {
let mut world = PhysicsWorld::<4>::new();
let body = RigidBody::new(1.0)
.with_velocity(Vector3::new(10.0, 0.0, 0.0))
.with_damping(0.1);
let id = world.add_body(body).unwrap();
world.step::<4>(1.0);
let b = world.body(id).unwrap();
assert!(b.velocity.x < 10.0);
assert!(b.velocity.x > 0.0);
}
#[test]
fn test_step_fixed_substeps() {
let mut world_single = PhysicsWorld::<4>::new();
let mut world_sub = PhysicsWorld::<4>::new();
world_single.set_gravity(Vector3::new(0.0, -10.0, 0.0));
world_sub.set_gravity(Vector3::new(0.0, -10.0, 0.0));
let body = RigidBody::new(1.0)
.with_position(Vector3::new(0.0, 100.0, 0.0))
.with_damping(0.0);
let id1 = world_single.add_body(body.clone()).unwrap();
let id2 = world_sub.add_body(body).unwrap();
world_single.step::<4>(1.0);
world_sub.step_fixed::<4>(1.0, 10);
let b1 = world_single.body(id1).unwrap();
let b2 = world_sub.body(id2).unwrap();
assert!(approx_eq(b1.velocity.y, b2.velocity.y));
}
#[test]
fn test_bodies_iterator() {
let mut world = PhysicsWorld::<4>::new();
world
.add_body(RigidBody::new(1.0).with_position(Vector3::new(1.0, 0.0, 0.0)))
.unwrap();
world
.add_body(RigidBody::new(2.0).with_position(Vector3::new(2.0, 0.0, 0.0)))
.unwrap();
let positions: std::vec::Vec<f32> = world.bodies().map(|(_, b)| b.position.x).collect();
assert_eq!(positions, std::vec![1.0, 2.0]);
}
#[test]
fn test_projectile_motion() {
let mut world = PhysicsWorld::<4>::new();
world.set_gravity(Vector3::new(0.0, -10.0, 0.0));
let body = RigidBody::new(1.0)
.with_velocity(Vector3::new(10.0, 10.0, 0.0))
.with_damping(0.0);
let id = world.add_body(body).unwrap();
for _ in 0..200 {
world.step::<4>(0.01);
}
let b = world.body(id).unwrap();
assert!((b.position.x - 20.0).abs() < 0.5);
assert!((b.velocity.y - (-10.0)).abs() < 0.5);
}
#[test]
fn test_sphere_sphere_no_collision() {
let result = collide_sphere_sphere(
&Vector3::new(0.0, 0.0, 0.0),
1.0,
&Vector3::new(3.0, 0.0, 0.0),
1.0,
);
assert!(result.is_none());
}
#[test]
fn test_sphere_sphere_touching() {
let result = collide_sphere_sphere(
&Vector3::new(0.0, 0.0, 0.0),
1.0,
&Vector3::new(2.0, 0.0, 0.0),
1.0,
);
assert!(result.is_none());
}
#[test]
fn test_sphere_sphere_overlapping() {
let result = collide_sphere_sphere(
&Vector3::new(0.0, 0.0, 0.0),
1.0,
&Vector3::new(1.5, 0.0, 0.0),
1.0,
);
let (normal, penetration) = result.unwrap();
assert!(approx_eq(penetration, 0.5));
assert!(normal.x > 0.0);
assert!(approx_eq(normal.y, 0.0));
}
#[test]
fn test_sphere_sphere_coincident() {
let result = collide_sphere_sphere(
&Vector3::new(0.0, 0.0, 0.0),
1.0,
&Vector3::new(0.0, 0.0, 0.0),
1.0,
);
let (normal, penetration) = result.unwrap();
assert!(approx_eq(penetration, 2.0));
assert!(approx_eq(normal.norm(), 1.0));
}
#[test]
fn test_aabb_aabb_no_collision() {
let result = collide_aabb_aabb(
&Vector3::new(0.0, 0.0, 0.0),
&Vector3::new(1.0, 1.0, 1.0),
&Vector3::new(3.0, 0.0, 0.0),
&Vector3::new(1.0, 1.0, 1.0),
);
assert!(result.is_none());
}
#[test]
fn test_aabb_aabb_overlapping_x() {
let result = collide_aabb_aabb(
&Vector3::new(0.0, 0.0, 0.0),
&Vector3::new(1.0, 1.0, 1.0),
&Vector3::new(1.5, 0.0, 0.0),
&Vector3::new(1.0, 1.0, 1.0),
);
let (normal, penetration) = result.unwrap();
assert!(approx_eq(penetration, 0.5));
assert!(approx_eq(normal.x, 1.0));
}
#[test]
fn test_aabb_aabb_overlapping_y() {
let result = collide_aabb_aabb(
&Vector3::new(0.0, 0.0, 0.0),
&Vector3::new(1.0, 1.0, 1.0),
&Vector3::new(0.0, 1.5, 0.0),
&Vector3::new(1.0, 1.0, 1.0),
);
let (normal, penetration) = result.unwrap();
assert!(approx_eq(penetration, 0.5));
assert!(approx_eq(normal.y, 1.0));
}
#[test]
fn test_aabb_aabb_negative_direction() {
let result = collide_aabb_aabb(
&Vector3::new(0.0, 0.0, 0.0),
&Vector3::new(1.0, 1.0, 1.0),
&Vector3::new(-1.5, 0.0, 0.0),
&Vector3::new(1.0, 1.0, 1.0),
);
let (normal, _) = result.unwrap();
assert!(approx_eq(normal.x, -1.0));
}
#[test]
fn test_sphere_aabb_no_collision() {
let result = collide_sphere_aabb(
&Vector3::new(5.0, 0.0, 0.0),
1.0,
&Vector3::new(0.0, 0.0, 0.0),
&Vector3::new(1.0, 1.0, 1.0),
);
assert!(result.is_none());
}
#[test]
fn test_sphere_aabb_overlapping() {
let result = collide_sphere_aabb(
&Vector3::new(1.5, 0.0, 0.0),
1.0,
&Vector3::new(0.0, 0.0, 0.0),
&Vector3::new(1.0, 1.0, 1.0),
);
let (normal, penetration) = result.unwrap();
assert!(approx_eq(penetration, 0.5));
assert!(normal.x < 0.0);
}
#[test]
fn test_sphere_aabb_sphere_inside() {
let result = collide_sphere_aabb(
&Vector3::new(0.0, 0.0, 0.0),
0.5,
&Vector3::new(0.0, 0.0, 0.0),
&Vector3::new(2.0, 1.0, 3.0),
);
let (normal, penetration) = result.unwrap();
assert!(penetration > 0.0);
assert!(approx_eq(normal.norm(), 1.0));
}
#[test]
fn test_sphere_aabb_from_below() {
let result = collide_sphere_aabb(
&Vector3::new(0.0, -1.5, 0.0),
1.0,
&Vector3::new(0.0, 0.0, 0.0),
&Vector3::new(2.0, 1.0, 2.0),
);
let (normal, penetration) = result.unwrap();
assert!(penetration > 0.0);
assert!(normal.y > 0.0);
}
#[test]
fn test_detect_collisions_no_colliders() {
let mut world = PhysicsWorld::<4>::new();
world.add_body(RigidBody::new(1.0)).unwrap();
world.add_body(RigidBody::new(1.0)).unwrap();
let contacts = world.detect_collisions::<4>();
assert_eq!(contacts.len(), 0);
}
#[test]
fn test_detect_collisions_sphere_sphere() {
let mut world = PhysicsWorld::<4>::new();
world
.add_body(
RigidBody::new(1.0)
.with_position(Vector3::new(0.0, 0.0, 0.0))
.with_collider(Collider::Sphere { radius: 1.0 }),
)
.unwrap();
world
.add_body(
RigidBody::new(1.0)
.with_position(Vector3::new(1.5, 0.0, 0.0))
.with_collider(Collider::Sphere { radius: 1.0 }),
)
.unwrap();
let contacts = world.detect_collisions::<4>();
assert_eq!(contacts.len(), 1);
assert!(approx_eq(contacts[0].penetration, 0.5));
}
#[test]
fn test_detect_collisions_skips_static_pairs() {
let mut world = PhysicsWorld::<4>::new();
world
.add_body(
RigidBody::new_static()
.with_position(Vector3::new(0.0, 0.0, 0.0))
.with_collider(Collider::Sphere { radius: 1.0 }),
)
.unwrap();
world
.add_body(
RigidBody::new_static()
.with_position(Vector3::new(0.5, 0.0, 0.0))
.with_collider(Collider::Sphere { radius: 1.0 }),
)
.unwrap();
let contacts = world.detect_collisions::<4>();
assert_eq!(contacts.len(), 0);
}
#[test]
fn test_collision_response_separates_bodies() {
let mut world = PhysicsWorld::<4>::new();
world
.add_body(
RigidBody::new(1.0)
.with_position(Vector3::new(0.0, 0.0, 0.0))
.with_collider(Collider::Sphere { radius: 1.0 })
.with_restitution(0.0)
.with_damping(0.0),
)
.unwrap();
world
.add_body(
RigidBody::new(1.0)
.with_position(Vector3::new(1.0, 0.0, 0.0))
.with_collider(Collider::Sphere { radius: 1.0 })
.with_restitution(0.0)
.with_damping(0.0),
)
.unwrap();
let contacts = world.detect_collisions::<4>();
assert_eq!(contacts.len(), 1);
world.resolve_contacts(&contacts);
let a = &world.body(BodyId(0)).unwrap().position;
let b = &world.body(BodyId(1)).unwrap().position;
let dist = (b - a).norm();
assert!(dist >= 2.0 - EPSILON);
}
#[test]
fn test_collision_response_bounce() {
let mut world = PhysicsWorld::<4>::new();
world
.add_body(
RigidBody::new(1.0)
.with_position(Vector3::new(0.0, 0.0, 0.0))
.with_velocity(Vector3::new(5.0, 0.0, 0.0))
.with_collider(Collider::Sphere { radius: 1.0 })
.with_restitution(1.0)
.with_damping(0.0),
)
.unwrap();
world
.add_body(
RigidBody::new(1.0)
.with_position(Vector3::new(1.5, 0.0, 0.0))
.with_velocity(Vector3::zeros())
.with_collider(Collider::Sphere { radius: 1.0 })
.with_restitution(1.0)
.with_damping(0.0),
)
.unwrap();
let contacts = world.detect_collisions::<4>();
world.resolve_contacts(&contacts);
let vel_a = world.body(BodyId(0)).unwrap().velocity;
let vel_b = world.body(BodyId(1)).unwrap().velocity;
assert!(approx_eq(vel_a.x, 0.0));
assert!(approx_eq(vel_b.x, 5.0));
}
#[test]
fn test_collision_dynamic_vs_static() {
let mut world = PhysicsWorld::<4>::new();
world
.add_body(
RigidBody::new(1.0)
.with_position(Vector3::new(0.0, 0.5, 0.0))
.with_velocity(Vector3::new(0.0, -5.0, 0.0))
.with_collider(Collider::Sphere { radius: 1.0 })
.with_restitution(0.5)
.with_damping(0.0),
)
.unwrap();
world
.add_body(
RigidBody::new_static()
.with_position(Vector3::new(0.0, -1.0, 0.0))
.with_collider(Collider::Aabb {
half_extents: Vector3::new(10.0, 1.0, 10.0),
})
.with_restitution(1.0),
)
.unwrap();
let contacts = world.detect_collisions::<4>();
assert_eq!(contacts.len(), 1);
world.resolve_contacts(&contacts);
let ball = world.body(BodyId(0)).unwrap();
assert!(ball.velocity.y > 0.0);
let floor = world.body(BodyId(1)).unwrap();
assert!(approx_vec_eq(&floor.velocity, &Vector3::zeros()));
assert!(approx_eq(floor.position.y, -1.0));
}
#[test]
fn test_step_with_collisions() {
let mut world = PhysicsWorld::<4>::new();
world.set_gravity(Vector3::new(0.0, -10.0, 0.0));
world
.add_body(
RigidBody::new(1.0)
.with_position(Vector3::new(0.0, 2.0, 0.0))
.with_collider(Collider::Sphere { radius: 0.5 })
.with_restitution(0.5)
.with_damping(0.0),
)
.unwrap();
world
.add_body(
RigidBody::new_static()
.with_position(Vector3::new(0.0, 0.0, 0.0))
.with_collider(Collider::Aabb {
half_extents: Vector3::new(10.0, 0.1, 10.0),
}),
)
.unwrap();
for _ in 0..600 {
world.step::<4>(1.0 / 60.0);
}
let ball = world.body(BodyId(0)).unwrap();
assert!(ball.position.y >= 0.5);
}
#[test]
fn test_collider_builder() {
let body = RigidBody::new(1.0).with_collider(Collider::Sphere { radius: 2.0 });
assert_eq!(body.collider, Some(Collider::Sphere { radius: 2.0 }));
let body2 = RigidBody::new(1.0).with_collider(Collider::Aabb {
half_extents: Vector3::new(1.0, 2.0, 3.0),
});
assert!(matches!(body2.collider, Some(Collider::Aabb { .. })));
}
#[test]
fn test_no_collider_body_ignored() {
let mut world = PhysicsWorld::<4>::new();
world.add_body(RigidBody::new(1.0)).unwrap();
world
.add_body(RigidBody::new(1.0).with_collider(Collider::Sphere { radius: 100.0 }))
.unwrap();
let contacts = world.detect_collisions::<4>();
assert_eq!(contacts.len(), 0); }
#[test]
fn test_aabb_sphere_collision_via_world() {
let mut world = PhysicsWorld::<4>::new();
world
.add_body(
RigidBody::new(1.0)
.with_position(Vector3::new(0.0, 0.0, 0.0))
.with_collider(Collider::Aabb {
half_extents: Vector3::new(1.0, 1.0, 1.0),
}),
)
.unwrap();
world
.add_body(
RigidBody::new(1.0)
.with_position(Vector3::new(1.5, 0.0, 0.0))
.with_collider(Collider::Sphere { radius: 1.0 }),
)
.unwrap();
let contacts = world.detect_collisions::<4>();
assert_eq!(contacts.len(), 1);
assert!(contacts[0].penetration > 0.0);
}
#[test]
fn test_friction_builder() {
let body = RigidBody::new(1.0).with_friction(0.7);
assert!(approx_eq(body.friction, 0.7));
}
#[test]
fn test_friction_clamped() {
let body = RigidBody::new(1.0).with_friction(2.0);
assert!(approx_eq(body.friction, 1.0));
let body2 = RigidBody::new(1.0).with_friction(-0.5);
assert!(approx_eq(body2.friction, 0.0));
}
#[test]
fn test_default_friction_values() {
let dynamic = RigidBody::new(1.0);
assert!(approx_eq(dynamic.friction, 0.3));
let static_body = RigidBody::new_static();
assert!(approx_eq(static_body.friction, 0.5));
}
#[test]
fn test_friction_reduces_tangential_velocity() {
let mut world = PhysicsWorld::<4>::new();
world
.add_body(
RigidBody::new(1.0)
.with_position(Vector3::new(0.0, 0.55, 0.0))
.with_velocity(Vector3::new(10.0, -0.1, 0.0))
.with_collider(Collider::Sphere { radius: 0.5 })
.with_restitution(0.0)
.with_friction(0.8)
.with_damping(0.0),
)
.unwrap();
world
.add_body(
RigidBody::new_static()
.with_position(Vector3::new(0.0, 0.0, 0.0))
.with_collider(Collider::Aabb {
half_extents: Vector3::new(50.0, 0.1, 50.0),
})
.with_friction(0.8),
)
.unwrap();
let initial_vx = 10.0_f32;
let contacts = world.detect_collisions::<4>();
assert_eq!(contacts.len(), 1);
world.resolve_contacts(&contacts);
let ball = world.body(BodyId(0)).unwrap();
assert!(ball.velocity.x < initial_vx);
assert!(ball.velocity.x >= 0.0); }
#[test]
fn test_zero_friction_preserves_tangential_velocity() {
let mut world = PhysicsWorld::<4>::new();
world
.add_body(
RigidBody::new(1.0)
.with_position(Vector3::new(0.0, 0.55, 0.0))
.with_velocity(Vector3::new(10.0, -1.0, 0.0))
.with_collider(Collider::Sphere { radius: 0.5 })
.with_restitution(0.0)
.with_friction(0.0)
.with_damping(0.0),
)
.unwrap();
world
.add_body(
RigidBody::new_static()
.with_position(Vector3::new(0.0, 0.0, 0.0))
.with_collider(Collider::Aabb {
half_extents: Vector3::new(50.0, 0.1, 50.0),
})
.with_friction(0.0),
)
.unwrap();
let contacts = world.detect_collisions::<4>();
world.resolve_contacts(&contacts);
let ball = world.body(BodyId(0)).unwrap();
assert!(approx_eq(ball.velocity.x, 10.0));
}
#[test]
fn test_high_friction_vs_low_friction() {
let make_world = |friction: f32| -> PhysicsWorld<4> {
let mut world = PhysicsWorld::<4>::new();
world
.add_body(
RigidBody::new(1.0)
.with_position(Vector3::new(0.0, 0.55, 0.0))
.with_velocity(Vector3::new(10.0, -1.0, 0.0))
.with_collider(Collider::Sphere { radius: 0.5 })
.with_restitution(0.0)
.with_friction(friction)
.with_damping(0.0),
)
.unwrap();
world
.add_body(
RigidBody::new_static()
.with_position(Vector3::new(0.0, 0.0, 0.0))
.with_collider(Collider::Aabb {
half_extents: Vector3::new(50.0, 0.1, 50.0),
})
.with_friction(friction),
)
.unwrap();
world
};
let mut world_low = make_world(0.1);
let mut world_high = make_world(1.0);
let contacts_low = world_low.detect_collisions::<4>();
world_low.resolve_contacts(&contacts_low);
let contacts_high = world_high.detect_collisions::<4>();
world_high.resolve_contacts(&contacts_high);
let vx_low = world_low.body(BodyId(0)).unwrap().velocity.x;
let vx_high = world_high.body(BodyId(0)).unwrap().velocity.x;
assert!(vx_high < vx_low);
}
#[test]
fn test_body_active_by_default() {
let body = RigidBody::new(1.0);
assert!(body.active);
let static_body = RigidBody::new_static();
assert!(static_body.active);
}
#[test]
fn test_remove_body() {
let mut world = PhysicsWorld::<4>::new();
let id = world
.add_body(RigidBody::new(1.0).with_velocity(Vector3::new(5.0, 0.0, 0.0)))
.unwrap();
assert_eq!(world.active_body_count(), 1);
assert!(world.remove_body(id));
assert_eq!(world.active_body_count(), 0);
assert_eq!(world.body_count(), 1);
let body = world.body(id).unwrap();
assert!(!body.active);
assert!(approx_vec_eq(&body.velocity, &Vector3::zeros()));
}
#[test]
fn test_remove_body_twice_returns_false() {
let mut world = PhysicsWorld::<4>::new();
let id = world.add_body(RigidBody::new(1.0)).unwrap();
assert!(world.remove_body(id));
assert!(!world.remove_body(id)); }
#[test]
fn test_remove_body_invalid_id() {
let mut world = PhysicsWorld::<4>::new();
assert!(!world.remove_body(BodyId(99)));
}
#[test]
fn test_inactive_body_not_integrated() {
let mut world = PhysicsWorld::<4>::new();
world.set_gravity(Vector3::new(0.0, -10.0, 0.0));
let id = world
.add_body(
RigidBody::new(1.0)
.with_position(Vector3::new(0.0, 10.0, 0.0))
.with_damping(0.0),
)
.unwrap();
world.remove_body(id);
world.step::<4>(1.0);
let body = world.body(id).unwrap();
assert!(approx_eq(body.position.y, 10.0));
}
#[test]
fn test_inactive_body_not_collided() {
let mut world = PhysicsWorld::<4>::new();
let id_a = world
.add_body(
RigidBody::new(1.0)
.with_position(Vector3::zeros())
.with_collider(Collider::Sphere { radius: 1.0 }),
)
.unwrap();
world
.add_body(
RigidBody::new(1.0)
.with_position(Vector3::new(0.5, 0.0, 0.0))
.with_collider(Collider::Sphere { radius: 1.0 }),
)
.unwrap();
let contacts = world.detect_collisions::<4>();
assert_eq!(contacts.len(), 1);
world.remove_body(id_a);
let contacts = world.detect_collisions::<4>();
assert_eq!(contacts.len(), 0);
}
#[test]
fn test_set_active_reactivates_body() {
let mut world = PhysicsWorld::<4>::new();
world.set_gravity(Vector3::new(0.0, -10.0, 0.0));
let id = world
.add_body(
RigidBody::new(1.0)
.with_position(Vector3::new(0.0, 10.0, 0.0))
.with_damping(0.0),
)
.unwrap();
world.remove_body(id);
world.step::<4>(1.0);
assert!(approx_eq(world.body(id).unwrap().position.y, 10.0));
world.set_active(id, true);
assert_eq!(world.active_body_count(), 1);
world.step::<4>(1.0);
assert!(world.body(id).unwrap().position.y < 10.0);
}
#[test]
fn test_active_body_count() {
let mut world = PhysicsWorld::<8>::new();
let id0 = world.add_body(RigidBody::new(1.0)).unwrap();
let id1 = world.add_body(RigidBody::new(1.0)).unwrap();
let _id2 = world.add_body(RigidBody::new(1.0)).unwrap();
assert_eq!(world.active_body_count(), 3);
world.remove_body(id0);
assert_eq!(world.active_body_count(), 2);
world.remove_body(id1);
assert_eq!(world.active_body_count(), 1);
}
#[test]
fn test_remove_preserves_other_body_ids() {
let mut world = PhysicsWorld::<4>::new();
world.set_gravity(Vector3::new(0.0, -10.0, 0.0));
let id0 = world
.add_body(
RigidBody::new(1.0)
.with_position(Vector3::new(0.0, 10.0, 0.0))
.with_damping(0.0),
)
.unwrap();
let id1 = world
.add_body(
RigidBody::new(1.0)
.with_position(Vector3::new(5.0, 10.0, 0.0))
.with_damping(0.0),
)
.unwrap();
world.remove_body(id0);
world.step::<4>(1.0);
assert!(approx_eq(world.body(id0).unwrap().position.y, 10.0));
assert!(world.body(id1).unwrap().position.y < 10.0);
}
#[test]
fn test_default_orientation_is_identity() {
let body = RigidBody::new(1.0);
assert!(approx_eq(body.orientation.w, 1.0));
assert!(approx_vec_eq(&body.angular_velocity, &Vector3::zeros()));
}
#[test]
fn test_angular_velocity_builder() {
let body = RigidBody::new(1.0).with_angular_velocity(Vector3::new(0.0, 5.0, 0.0));
assert!(approx_eq(body.angular_velocity.y, 5.0));
}
#[test]
fn test_angular_damping_builder() {
let body = RigidBody::new(1.0).with_angular_damping(0.05);
assert!(approx_eq(body.angular_damping, 0.05));
}
#[test]
fn test_angular_damping_clamped() {
let body = RigidBody::new(1.0).with_angular_damping(2.0);
assert!(approx_eq(body.angular_damping, 1.0));
let body2 = RigidBody::new(1.0).with_angular_damping(-1.0);
assert!(approx_eq(body2.angular_damping, 0.0));
}
#[test]
fn test_inertia_sphere() {
let body = RigidBody::new(2.0).with_inertia_sphere(0.5);
let inv_i = body.inv_inertia_local[(0, 0)];
assert!(approx_eq(inv_i, 5.0));
assert!(approx_eq(body.inv_inertia_local[(1, 1)], 5.0));
assert!(approx_eq(body.inv_inertia_local[(2, 2)], 5.0));
assert!(approx_eq(body.inv_inertia_local[(0, 1)], 0.0));
}
#[test]
fn test_inertia_box() {
let body = RigidBody::new(12.0).with_inertia_box(Vector3::new(1.0, 2.0, 3.0));
assert!(approx_eq(body.inv_inertia_local[(0, 0)], 1.0 / 52.0));
assert!(approx_eq(body.inv_inertia_local[(1, 1)], 1.0 / 40.0));
assert!(approx_eq(body.inv_inertia_local[(2, 2)], 1.0 / 20.0));
}
#[test]
fn test_inertia_static_body_unchanged() {
let body = RigidBody::new_static().with_inertia_sphere(1.0);
assert!(approx_eq(body.inv_inertia_local[(0, 0)], 0.0));
}
#[test]
fn test_apply_torque() {
let mut body = RigidBody::new(1.0);
body.apply_torque(Vector3::new(1.0, 0.0, 0.0));
body.apply_torque(Vector3::new(0.0, 2.0, 0.0));
assert!(approx_vec_eq(
&body.torque_accumulator,
&Vector3::new(1.0, 2.0, 0.0)
));
}
#[test]
fn test_apply_angular_impulse() {
let mut body = RigidBody::new(1.0).with_inertia_sphere(1.0);
body.apply_angular_impulse(Vector3::new(1.0, 0.0, 0.0));
assert!(approx_eq(body.angular_velocity.x, 2.5));
}
#[test]
fn test_angular_impulse_on_static_body_ignored() {
let mut body = RigidBody::new_static();
body.apply_angular_impulse(Vector3::new(100.0, 0.0, 0.0));
assert!(approx_vec_eq(&body.angular_velocity, &Vector3::zeros()));
}
#[test]
fn test_constant_angular_velocity_changes_orientation() {
let mut world = PhysicsWorld::<4>::new();
let id = world
.add_body(
RigidBody::new(1.0)
.with_angular_velocity(Vector3::new(0.0, core::f32::consts::PI, 0.0))
.with_angular_damping(0.0)
.with_damping(0.0),
)
.unwrap();
for _ in 0..100 {
world.step::<4>(0.01);
}
let body = world.body(id).unwrap();
assert!(body.orientation.w.abs() < 0.9); assert!(approx_eq(body.angular_velocity.y, core::f32::consts::PI));
}
#[test]
fn test_torque_produces_angular_acceleration() {
let mut world = PhysicsWorld::<4>::new();
let id = world
.add_body(
RigidBody::new(1.0)
.with_inertia_sphere(1.0)
.with_angular_damping(0.0)
.with_damping(0.0),
)
.unwrap();
world
.body_mut(id)
.unwrap()
.apply_torque(Vector3::new(0.0, 0.0, 1.0));
world.step::<4>(1.0);
let body = world.body(id).unwrap();
assert!(approx_eq(body.angular_velocity.z, 2.5));
}
#[test]
fn test_torque_cleared_after_step() {
let mut world = PhysicsWorld::<4>::new();
let id = world
.add_body(
RigidBody::new(1.0)
.with_inertia_sphere(1.0)
.with_angular_damping(0.0)
.with_damping(0.0),
)
.unwrap();
world
.body_mut(id)
.unwrap()
.apply_torque(Vector3::new(0.0, 0.0, 1.0));
world.step::<4>(1.0);
let omega_before = world.body(id).unwrap().angular_velocity.z;
world.step::<4>(1.0);
let omega_after = world.body(id).unwrap().angular_velocity.z;
assert!(approx_eq(omega_before, omega_after));
}
#[test]
fn test_angular_damping_reduces_spin() {
let mut world = PhysicsWorld::<4>::new();
let id = world
.add_body(
RigidBody::new(1.0)
.with_angular_velocity(Vector3::new(10.0, 0.0, 0.0))
.with_angular_damping(0.1)
.with_damping(0.0),
)
.unwrap();
world.step::<4>(1.0);
let body = world.body(id).unwrap();
assert!(body.angular_velocity.x < 10.0);
assert!(body.angular_velocity.x > 0.0);
}
#[test]
fn test_orientation_stays_normalized() {
let mut world = PhysicsWorld::<4>::new();
let id = world
.add_body(
RigidBody::new(1.0)
.with_angular_velocity(Vector3::new(3.0, 5.0, 7.0))
.with_angular_damping(0.0)
.with_damping(0.0),
)
.unwrap();
for _ in 0..1000 {
world.step::<4>(0.01);
}
let body = world.body(id).unwrap();
let q = body.orientation.into_inner();
let norm = (q.w * q.w + q.i * q.i + q.j * q.j + q.k * q.k).sqrt();
assert!((norm - 1.0).abs() < 1e-3);
}
#[test]
fn test_collision_imparts_angular_velocity() {
let mut world = PhysicsWorld::<4>::new();
world
.add_body(
RigidBody::new(1.0)
.with_position(Vector3::new(0.0, 0.55, 0.0))
.with_velocity(Vector3::new(5.0, -1.0, 0.0))
.with_collider(Collider::Sphere { radius: 0.5 })
.with_inertia_sphere(0.5)
.with_restitution(0.0)
.with_friction(0.8)
.with_damping(0.0)
.with_angular_damping(0.0),
)
.unwrap();
world
.add_body(
RigidBody::new_static()
.with_position(Vector3::new(0.0, 0.0, 0.0))
.with_collider(Collider::Aabb {
half_extents: Vector3::new(50.0, 0.1, 50.0),
})
.with_friction(0.8),
)
.unwrap();
let contacts = world.detect_collisions::<4>();
assert_eq!(contacts.len(), 1);
world.resolve_contacts(&contacts);
let ball = world.body(BodyId(0)).unwrap();
let omega_magnitude = ball.angular_velocity.norm();
assert!(
omega_magnitude > 0.01,
"Expected angular velocity from friction, got {}",
omega_magnitude
);
}
#[test]
fn test_remove_body_clears_angular_state() {
let mut world = PhysicsWorld::<4>::new();
let id = world
.add_body(RigidBody::new(1.0).with_angular_velocity(Vector3::new(5.0, 5.0, 5.0)))
.unwrap();
world.remove_body(id);
let body = world.body(id).unwrap();
assert!(approx_vec_eq(&body.angular_velocity, &Vector3::zeros()));
}
#[test]
fn test_inv_inertia_world_rotated() {
let mut body = RigidBody::new(1.0).with_inertia_box(Vector3::new(1.0, 2.0, 3.0));
let inv_i_local_00 = body.inv_inertia_local[(0, 0)];
let inv_i_world_00 = body.inv_inertia_world()[(0, 0)];
assert!(approx_eq(inv_i_local_00, inv_i_world_00));
body.orientation = UnitQuaternion::from_axis_angle(
&nalgebra::Unit::new_normalize(Vector3::new(0.0, 1.0, 0.0)),
core::f32::consts::FRAC_PI_2,
);
let inv_i_rotated = body.inv_inertia_world();
assert!(approx_eq(
inv_i_rotated[(0, 0)],
body.inv_inertia_local[(2, 2)]
));
assert!(approx_eq(
inv_i_rotated[(2, 2)],
body.inv_inertia_local[(0, 0)]
));
}
#[test]
fn test_elastic_collision_angular_conservation() {
let mut world = PhysicsWorld::<4>::new();
world
.add_body(
RigidBody::new(1.0)
.with_position(Vector3::new(0.0, 0.0, 0.0))
.with_velocity(Vector3::new(5.0, 0.0, 0.0))
.with_collider(Collider::Sphere { radius: 1.0 })
.with_inertia_sphere(1.0)
.with_restitution(1.0)
.with_friction(0.0)
.with_damping(0.0)
.with_angular_damping(0.0),
)
.unwrap();
world
.add_body(
RigidBody::new(1.0)
.with_position(Vector3::new(1.5, 0.0, 0.0))
.with_velocity(Vector3::zeros())
.with_collider(Collider::Sphere { radius: 1.0 })
.with_inertia_sphere(1.0)
.with_restitution(1.0)
.with_friction(0.0)
.with_damping(0.0)
.with_angular_damping(0.0),
)
.unwrap();
let contacts = world.detect_collisions::<4>();
world.resolve_contacts(&contacts);
let omega_a = world.body(BodyId(0)).unwrap().angular_velocity;
let omega_b = world.body(BodyId(1)).unwrap().angular_velocity;
assert!(omega_a.norm() < EPSILON);
assert!(omega_b.norm() < EPSILON);
}
#[test]
fn test_add_constraint() {
let mut world = PhysicsWorld::<4, 4>::new();
let a = world.add_body(RigidBody::new(1.0)).unwrap();
let b = world.add_body(RigidBody::new(1.0)).unwrap();
let cid = world
.add_constraint(Constraint::Distance {
body_a: a,
body_b: b,
anchor_a: Vector3::zeros(),
anchor_b: Vector3::zeros(),
rest_length: 2.0,
compliance: 0.0,
})
.unwrap();
assert_eq!(world.constraint_count(), 1);
assert!(world.constraint(cid).is_some());
}
#[test]
fn test_add_constraint_at_capacity() {
let mut world = PhysicsWorld::<4, 1>::new();
let a = world.add_body(RigidBody::new(1.0)).unwrap();
let b = world.add_body(RigidBody::new(1.0)).unwrap();
let c = Constraint::BallSocket {
body_a: a,
body_b: b,
anchor_a: Vector3::zeros(),
anchor_b: Vector3::zeros(),
compliance: 0.0,
};
assert!(world.add_constraint(c.clone()).is_some());
assert!(world.add_constraint(c).is_none()); }
#[test]
fn test_remove_constraint() {
let mut world = PhysicsWorld::<4, 4>::new();
let a = world.add_body(RigidBody::new(1.0)).unwrap();
let b = world.add_body(RigidBody::new(1.0)).unwrap();
let cid = world
.add_ball_socket(a, Vector3::zeros(), b, Vector3::zeros(), 0.0)
.unwrap();
assert_eq!(world.constraint_count(), 1);
assert!(world.remove_constraint(cid));
assert_eq!(world.constraint_count(), 0);
}
#[test]
fn test_remove_constraint_invalid_id() {
let mut world = PhysicsWorld::<4, 4>::new();
assert!(!world.remove_constraint(ConstraintId(99)));
}
#[test]
fn test_distance_constraint_maintains_length() {
let mut world = PhysicsWorld::<4, 4>::new();
world.set_gravity(Vector3::new(0.0, -10.0, 0.0));
let a = world
.add_body(
RigidBody::new(1.0)
.with_position(Vector3::new(0.0, 5.0, 0.0))
.with_damping(0.0)
.with_angular_damping(0.0),
)
.unwrap();
let b = world
.add_body(
RigidBody::new(1.0)
.with_position(Vector3::new(3.0, 5.0, 0.0))
.with_damping(0.0)
.with_angular_damping(0.0),
)
.unwrap();
world
.add_distance_constraint(a, Vector3::zeros(), b, Vector3::zeros(), 0.0)
.unwrap();
for _ in 0..200 {
world.step::<4>(1.0 / 60.0);
}
let pa = world.body(a).unwrap().position;
let pb = world.body(b).unwrap().position;
let dist = (pb - pa).norm();
assert!(
(dist - 3.0).abs() < 0.3,
"Expected distance ~3.0, got {}",
dist
);
}
#[test]
fn test_ball_socket_keeps_points_together() {
let mut world = PhysicsWorld::<4, 4>::new();
world.set_gravity(Vector3::new(0.0, -10.0, 0.0));
world.solver_iterations = 8;
let a = world
.add_body(RigidBody::new_static().with_position(Vector3::new(0.0, 10.0, 0.0)))
.unwrap();
let b = world
.add_body(
RigidBody::new(1.0)
.with_position(Vector3::new(0.0, 10.0, 0.0))
.with_damping(0.01)
.with_angular_damping(0.01),
)
.unwrap();
world
.add_ball_socket(a, Vector3::zeros(), b, Vector3::zeros(), 0.0)
.unwrap();
for _ in 0..120 {
world.step::<4>(1.0 / 60.0);
}
let pa = world.body(a).unwrap().position;
let pb = world.body(b).unwrap().position;
let gap = (pb - pa).norm();
assert!(gap < 1.0, "Ball-socket gap too large: {}", gap);
}
#[test]
fn test_pendulum_swings_under_gravity() {
let mut world = PhysicsWorld::<4, 4>::new();
world.set_gravity(Vector3::new(0.0, -10.0, 0.0));
let anchor = world
.add_body(RigidBody::new_static().with_position(Vector3::new(0.0, 10.0, 0.0)))
.unwrap();
let bob = world
.add_body(
RigidBody::new(1.0)
.with_position(Vector3::new(3.0, 10.0, 0.0))
.with_damping(0.0)
.with_angular_damping(0.0),
)
.unwrap();
world
.add_distance_constraint(anchor, Vector3::zeros(), bob, Vector3::zeros(), 0.0)
.unwrap();
let initial_y = 10.0;
for _ in 0..120 {
world.step::<4>(1.0 / 60.0);
}
let bob_pos = world.body(bob).unwrap().position;
assert!(
bob_pos.y < initial_y,
"Bob should swing down, y={}",
bob_pos.y
);
}
#[test]
fn test_soft_distance_constraint() {
let mut world = PhysicsWorld::<4, 4>::new();
world.set_gravity(Vector3::zeros());
let a = world
.add_body(
RigidBody::new(1.0)
.with_position(Vector3::new(0.0, 0.0, 0.0))
.with_damping(0.0)
.with_angular_damping(0.0),
)
.unwrap();
let b = world
.add_body(
RigidBody::new(1.0)
.with_position(Vector3::new(5.0, 0.0, 0.0))
.with_damping(0.0)
.with_angular_damping(0.0),
)
.unwrap();
world
.add_constraint(Constraint::Distance {
body_a: a,
body_b: b,
anchor_a: Vector3::zeros(),
anchor_b: Vector3::zeros(),
rest_length: 2.0,
compliance: 0.01,
})
.unwrap();
for _ in 0..60 {
world.step::<4>(1.0 / 60.0);
}
let pa = world.body(a).unwrap().position;
let pb = world.body(b).unwrap().position;
let dist = (pb - pa).norm();
assert!(dist < 5.0, "Spring should have contracted, dist={}", dist);
}
#[test]
fn test_fixed_joint_preserves_relative_orientation() {
let mut world = PhysicsWorld::<4, 4>::new();
world.set_gravity(Vector3::new(0.0, -10.0, 0.0));
let a = world
.add_body(RigidBody::new_static().with_position(Vector3::new(0.0, 10.0, 0.0)))
.unwrap();
let b = world
.add_body(
RigidBody::new(1.0)
.with_position(Vector3::new(2.0, 10.0, 0.0))
.with_inertia_box(Vector3::new(0.5, 0.5, 0.5))
.with_damping(0.0)
.with_angular_damping(0.0),
)
.unwrap();
world
.add_fixed_joint(a, Vector3::new(2.0, 0.0, 0.0), b, Vector3::zeros(), 0.0)
.unwrap();
let initial_qa = world.body(a).unwrap().orientation;
let initial_qb = world.body(b).unwrap().orientation;
let initial_rel = initial_qb * initial_qa.inverse();
for _ in 0..200 {
world.step::<4>(1.0 / 60.0);
}
let qa = world.body(a).unwrap().orientation;
let qb = world.body(b).unwrap().orientation;
let current_rel = qb * qa.inverse();
let error = current_rel * initial_rel.inverse();
let angle = error.angle();
assert!(angle < 0.5, "Fixed joint orientation drift: {} rad", angle);
}
#[test]
fn test_chain_of_distance_constraints() {
let mut world = PhysicsWorld::<8, 8>::new();
world.set_gravity(Vector3::new(0.0, -10.0, 0.0));
world.solver_iterations = 8;
let anchor = world
.add_body(RigidBody::new_static().with_position(Vector3::new(0.0, 10.0, 0.0)))
.unwrap();
let link1 = world
.add_body(
RigidBody::new(1.0)
.with_position(Vector3::new(0.0, 8.0, 0.0))
.with_damping(0.01)
.with_angular_damping(0.01),
)
.unwrap();
let link2 = world
.add_body(
RigidBody::new(1.0)
.with_position(Vector3::new(0.0, 6.0, 0.0))
.with_damping(0.01)
.with_angular_damping(0.01),
)
.unwrap();
world
.add_distance_constraint(anchor, Vector3::zeros(), link1, Vector3::zeros(), 0.0)
.unwrap();
world
.add_distance_constraint(link1, Vector3::zeros(), link2, Vector3::zeros(), 0.0)
.unwrap();
for _ in 0..600 {
world.step::<4>(1.0 / 60.0);
}
let pa = world.body(anchor).unwrap().position;
let p1 = world.body(link1).unwrap().position;
let p2 = world.body(link2).unwrap().position;
let d1 = (p1 - pa).norm();
let d2 = (p2 - p1).norm();
assert!((d1 - 2.0).abs() < 0.5, "Link 1 distance: {}", d1);
assert!((d2 - 2.0).abs() < 0.5, "Link 2 distance: {}", d2);
assert!(p1.y < pa.y, "Link 1 should be below anchor");
assert!(p2.y < p1.y, "Link 2 should be below link 1");
}
#[test]
fn test_constraint_between_equal_bodies_symmetric() {
let mut world = PhysicsWorld::<4, 4>::new();
world.set_gravity(Vector3::zeros());
let a = world
.add_body(
RigidBody::new(1.0)
.with_position(Vector3::new(-3.0, 0.0, 0.0))
.with_damping(0.0)
.with_angular_damping(0.0),
)
.unwrap();
let b = world
.add_body(
RigidBody::new(1.0)
.with_position(Vector3::new(3.0, 0.0, 0.0))
.with_damping(0.0)
.with_angular_damping(0.0),
)
.unwrap();
world
.add_constraint(Constraint::Distance {
body_a: a,
body_b: b,
anchor_a: Vector3::zeros(),
anchor_b: Vector3::zeros(),
rest_length: 2.0,
compliance: 0.0,
})
.unwrap();
for _ in 0..120 {
world.step::<4>(1.0 / 60.0);
}
let pa = world.body(a).unwrap().position;
let pb = world.body(b).unwrap().position;
let com = (pa + pb) / 2.0;
assert!(
com.norm() < 0.5,
"COM should stay near origin, got {:?}",
com
);
}
#[test]
fn test_constraint_with_static_body() {
let mut world = PhysicsWorld::<4, 4>::new();
world.set_gravity(Vector3::zeros());
let fixed = world
.add_body(RigidBody::new_static().with_position(Vector3::new(0.0, 0.0, 0.0)))
.unwrap();
let dynamic = world
.add_body(
RigidBody::new(1.0)
.with_position(Vector3::new(5.0, 0.0, 0.0))
.with_damping(0.0)
.with_angular_damping(0.0),
)
.unwrap();
world
.add_constraint(Constraint::Distance {
body_a: fixed,
body_b: dynamic,
anchor_a: Vector3::zeros(),
anchor_b: Vector3::zeros(),
rest_length: 2.0,
compliance: 0.0,
})
.unwrap();
for _ in 0..120 {
world.step::<4>(1.0 / 60.0);
}
let p_fixed = world.body(fixed).unwrap().position;
assert!(approx_vec_eq(&p_fixed, &Vector3::zeros()));
let p_dyn = world.body(dynamic).unwrap().position;
let dist = p_dyn.norm();
assert!((dist - 2.0).abs() < 0.5, "Expected ~2.0, got {}", dist);
}
#[test]
fn test_solver_iterations_affect_convergence() {
let make_world = |iterations: u32| -> PhysicsWorld<4, 4> {
let mut world = PhysicsWorld::<4, 4>::new();
world.set_gravity(Vector3::new(0.0, -10.0, 0.0));
world.solver_iterations = iterations;
let a = world
.add_body(RigidBody::new_static().with_position(Vector3::new(0.0, 10.0, 0.0)))
.unwrap();
let b = world
.add_body(
RigidBody::new(1.0)
.with_position(Vector3::new(3.0, 10.0, 0.0))
.with_damping(0.0)
.with_angular_damping(0.0),
)
.unwrap();
world
.add_distance_constraint(a, Vector3::zeros(), b, Vector3::zeros(), 0.0)
.unwrap();
world
};
let mut world_low = make_world(1);
let mut world_high = make_world(16);
for _ in 0..120 {
world_low.step::<4>(1.0 / 60.0);
world_high.step::<4>(1.0 / 60.0);
}
let pa_low = world_low.body(BodyId(0)).unwrap().position;
let pb_low = world_low.body(BodyId(1)).unwrap().position;
let error_low = ((pb_low - pa_low).norm() - 3.0).abs();
let pa_high = world_high.body(BodyId(0)).unwrap().position;
let pb_high = world_high.body(BodyId(1)).unwrap().position;
let error_high = ((pb_high - pa_high).norm() - 3.0).abs();
assert!(
error_high <= error_low + EPSILON,
"High iterations error {} should be <= low iterations error {}",
error_high,
error_low
);
}
#[test]
fn test_no_constraints_no_effect() {
let mut world = PhysicsWorld::<4, 4>::new();
world.set_gravity(Vector3::new(0.0, -10.0, 0.0));
let id = world
.add_body(
RigidBody::new(1.0)
.with_position(Vector3::new(0.0, 10.0, 0.0))
.with_damping(0.0),
)
.unwrap();
world.step::<4>(1.0);
let body = world.body(id).unwrap();
assert!(approx_eq(body.velocity.y, -10.0));
}
#[test]
fn test_add_distance_constraint_helper() {
let mut world = PhysicsWorld::<4, 4>::new();
let a = world
.add_body(RigidBody::new(1.0).with_position(Vector3::new(0.0, 0.0, 0.0)))
.unwrap();
let b = world
.add_body(RigidBody::new(1.0).with_position(Vector3::new(5.0, 0.0, 0.0)))
.unwrap();
let cid = world
.add_distance_constraint(a, Vector3::zeros(), b, Vector3::zeros(), 0.0)
.unwrap();
if let Constraint::Distance { rest_length, .. } = world.constraint(cid).unwrap() {
assert!(approx_eq(*rest_length, 5.0));
} else {
panic!("Expected Distance constraint");
}
}
#[test]
fn test_broad_phase_skips_distant_bodies() {
let mut world = PhysicsWorld::<4>::new();
world
.add_body(
RigidBody::new(1.0)
.with_position(Vector3::new(0.0, 0.0, 0.0))
.with_collider(Collider::Sphere { radius: 1.0 }),
)
.unwrap();
world
.add_body(
RigidBody::new(1.0)
.with_position(Vector3::new(100.0, 0.0, 0.0))
.with_collider(Collider::Sphere { radius: 1.0 }),
)
.unwrap();
let contacts = world.detect_collisions::<4>();
assert!(contacts.is_empty(), "Distant bodies should not collide");
}
#[test]
fn test_broad_phase_regression() {
let mut world = PhysicsWorld::<4>::new();
world
.add_body(
RigidBody::new(1.0)
.with_position(Vector3::new(0.0, 0.0, 0.0))
.with_collider(Collider::Sphere { radius: 1.0 }),
)
.unwrap();
world
.add_body(
RigidBody::new(1.0)
.with_position(Vector3::new(1.5, 0.0, 0.0))
.with_collider(Collider::Sphere { radius: 1.0 }),
)
.unwrap();
let contacts = world.detect_collisions::<4>();
assert_eq!(
contacts.len(),
1,
"Overlapping spheres should produce one contact"
);
assert!(contacts[0].penetration > 0.0);
}
}