use crate::body::RigidBody;
use nalgebra::SVector;
use symtropy_math::{Bivector, Point, Rotor};
pub fn integrate<const D: usize>(body: &mut RigidBody<D>, gravity: &SVector<f64, D>, dt: f64) {
if !body.is_dynamic() {
body.clear_accumulators();
return;
}
if !body.force_accumulator.iter().all(|v| v.is_finite()) {
body.force_accumulator = SVector::zeros();
}
if !body.torque_accumulator.is_finite() {
body.torque_accumulator = Bivector::zero();
}
let accel = body.force_accumulator * body.inv_mass + gravity;
body.linear_velocity += accel * dt;
if body.linear_damping > 1e-15 {
let tau = 1.0 / body.linear_damping;
body.linear_velocity *= (-dt / tau).exp();
}
let speed = body.linear_velocity.norm();
if speed > 1000.0 {
body.linear_velocity *= 1000.0 / speed;
}
let displacement = body.linear_velocity * dt;
body.transform.translation = Point(body.transform.translation.0 + displacement);
let inv_i_avg = body.inv_inertia.sum() / D as f64;
let ang_accel = body.torque_accumulator.scale(inv_i_avg);
body.angular_velocity = body.angular_velocity.add(&ang_accel.scale(dt));
if body.angular_damping > 1e-15 {
let ang_tau = 1.0 / body.angular_damping;
body.angular_velocity = body.angular_velocity.scale((-dt / ang_tau).exp());
}
let ang_speed = body.angular_velocity.norm();
if ang_speed > 100.0 {
body.angular_velocity = body.angular_velocity.scale(100.0 / ang_speed);
}
if ang_speed > 1e-12 {
let delta_angle = ang_speed * dt;
let delta_rotation = Rotor::from_plane_angle(&body.angular_velocity, delta_angle);
body.transform.rotation = delta_rotation.compose(&body.transform.rotation);
}
if !body.linear_velocity.iter().all(|v| v.is_finite()) {
debug_assert!(
false,
"NaN/Inf detected in linear_velocity after integration"
);
body.linear_velocity = SVector::zeros();
}
if !body.angular_velocity.is_finite() {
debug_assert!(
false,
"NaN/Inf detected in angular_velocity after integration"
);
body.angular_velocity = Bivector::zero();
}
if !body.transform.translation.0.iter().all(|v| v.is_finite()) {
debug_assert!(false, "NaN/Inf detected in position after integration");
body.linear_velocity = SVector::zeros();
}
body.clear_accumulators();
}
pub fn separate<const D: usize>(body: &mut RigidBody<D>, correction: &SVector<f64, D>) {
if body.is_dynamic() {
body.transform.translation = Point(body.transform.translation.0 + correction);
}
}
pub fn apply_impulse<const D: usize>(body: &mut RigidBody<D>, impulse: &SVector<f64, D>) {
if body.is_dynamic() {
body.linear_velocity += impulse * body.inv_mass;
}
}
pub fn apply_angular_impulse<const D: usize>(body: &mut RigidBody<D>, impulse: &Bivector<D>) {
if body.is_dynamic() {
let inv_i_avg = body.inv_inertia.sum() / D as f64;
body.angular_velocity = body.angular_velocity.add(&impulse.scale(inv_i_avg));
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::body::BodyHandle;
#[test]
fn free_fall_3d() {
let mut body = RigidBody::<3>::dynamic_sphere(BodyHandle(0), Point::origin(), 1.0, 1.0);
let gravity = SVector::from([0.0, -9.81, 0.0]);
let dt = 0.01;
for _ in 0..100 {
integrate(&mut body, &gravity, dt);
}
let y = body.transform.translation.coord(1);
assert!(y < -3.0, "y = {y}, expected < -3.0");
assert!(y > -6.0, "y = {y}, expected > -6.0 (damping bounds)");
}
#[test]
fn stationary_without_forces() {
let mut body =
RigidBody::<3>::dynamic_sphere(BodyHandle(0), Point::new([5.0, 5.0, 5.0]), 1.0, 1.0);
let zero_gravity = SVector::zeros();
let dt = 0.01;
for _ in 0..100 {
integrate(&mut body, &zero_gravity, dt);
}
assert!((body.transform.translation.coord(0) - 5.0).abs() < 0.1);
}
#[test]
fn applied_force_accelerates() {
let mut body = RigidBody::<3>::dynamic_sphere(
BodyHandle(0),
Point::origin(),
1.0,
2.0, );
let zero_gravity = SVector::zeros();
let dt = 0.01;
for _ in 0..100 {
body.apply_force(SVector::from([10.0, 0.0, 0.0]));
integrate(&mut body, &zero_gravity, dt);
}
let vx = body.linear_velocity[0];
assert!(vx > 3.0, "vx = {vx}, expected > 3.0");
assert!(vx < 6.0, "vx = {vx}, expected < 6.0");
}
#[test]
fn angular_velocity_rotates_body() {
let mut body = RigidBody::<3>::dynamic_sphere(BodyHandle(0), Point::origin(), 1.0, 1.0);
body.angular_velocity = Bivector::<3>::unit_plane(0, 1).scale(1.0);
let zero_gravity = SVector::zeros();
let dt = 0.01;
for _ in 0..100 {
integrate(&mut body, &zero_gravity, dt);
}
let mat = body.transform.rotation.to_matrix();
let trace = mat.trace();
assert!(trace < 2.99, "trace = {trace}, body didn't rotate");
}
#[test]
fn impulse_changes_velocity() {
let mut body = RigidBody::<4>::dynamic_sphere(
BodyHandle(0),
Point::origin(),
1.0,
5.0, );
apply_impulse(&mut body, &SVector::from([50.0, 0.0, 0.0, 0.0]));
assert!((body.linear_velocity[0] - 10.0).abs() < 1e-10);
}
#[test]
fn energy_conservation_no_damping() {
let mut body =
RigidBody::<3>::dynamic_sphere(BodyHandle(0), Point::new([0.0, 100.0, 0.0]), 1.0, 1.0);
body.linear_damping = 0.0;
body.angular_damping = 0.0;
let gravity = SVector::from([0.0, -9.81, 0.0]);
let dt = 0.001;
let initial_energy =
body.kinetic_energy() + body.mass * 9.81 * body.transform.translation.coord(1);
for _ in 0..1000 {
integrate(&mut body, &gravity, dt);
}
let final_energy =
body.kinetic_energy() + body.mass * 9.81 * body.transform.translation.coord(1);
let drift = ((final_energy - initial_energy) / initial_energy).abs();
assert!(drift < 0.01, "energy drift {drift} > 1%");
}
#[test]
fn static_body_not_affected() {
use symtropy_math::Sphere;
let mut body = RigidBody::<3>::static_body(
BodyHandle(0),
Point::new([5.0, 5.0, 5.0]),
Box::new(Sphere::unit()),
);
body.apply_force(SVector::from([1000.0, 0.0, 0.0]));
let gravity = SVector::from([0.0, -100.0, 0.0]);
integrate(&mut body, &gravity, 1.0);
assert!((body.transform.translation.coord(0) - 5.0).abs() < 1e-12);
}
#[test]
fn nan_force_is_sanitized() {
let mut body =
RigidBody::<3>::dynamic_sphere(BodyHandle(0), Point::new([1.0, 2.0, 3.0]), 1.0, 1.0);
body.apply_force(SVector::from([f64::NAN, 0.0, 0.0]));
let gravity = SVector::zeros();
integrate(&mut body, &gravity, 0.01);
assert!(
body.linear_velocity.iter().all(|v| v.is_finite()),
"NaN force propagated to velocity"
);
assert!(
body.transform.translation.0.iter().all(|v| v.is_finite()),
"NaN force propagated to position"
);
}
#[test]
fn inf_force_is_sanitized() {
let mut body = RigidBody::<3>::dynamic_sphere(BodyHandle(0), Point::origin(), 1.0, 1.0);
body.apply_force(SVector::from([f64::INFINITY, 0.0, 0.0]));
let gravity = SVector::zeros();
integrate(&mut body, &gravity, 0.01);
assert!(
body.linear_velocity.iter().all(|v| v.is_finite()),
"Inf force propagated to velocity"
);
}
}