#![allow(clippy::needless_range_loop)]
pub fn quat_rotate(q: [f32; 4], v: [f32; 3]) -> [f32; 3] {
let (qx, qy, qz, qw) = (q[0], q[1], q[2], q[3]);
let tx = 2.0 * (qy * v[2] - qz * v[1]);
let ty = 2.0 * (qz * v[0] - qx * v[2]);
let tz = 2.0 * (qx * v[1] - qy * v[0]);
[
v[0] + qw * tx + (qy * tz - qz * ty),
v[1] + qw * ty + (qz * tx - qx * tz),
v[2] + qw * tz + (qx * ty - qy * tx),
]
}
pub fn quat_mul(a: [f32; 4], b: [f32; 4]) -> [f32; 4] {
let (ax, ay, az, aw) = (a[0], a[1], a[2], a[3]);
let (bx, by, bz, bw) = (b[0], b[1], b[2], b[3]);
[
aw * bx + ax * bw + ay * bz - az * by,
aw * by - ax * bz + ay * bw + az * bx,
aw * bz + ax * by - ay * bx + az * bw,
aw * bw - ax * bx - ay * by - az * bz,
]
}
pub fn quat_normalize(q: [f32; 4]) -> [f32; 4] {
let norm = (q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]).sqrt();
if norm < 1e-9 {
return [0.0, 0.0, 0.0, 1.0];
}
[q[0] / norm, q[1] / norm, q[2] / norm, q[3] / norm]
}
pub fn integrate_orientation(q: [f32; 4], omega: [f32; 3], dt: f32) -> [f32; 4] {
let omega_q = [omega[0], omega[1], omega[2], 0.0_f32];
let dq = quat_mul(omega_q, q);
let q_new = [
q[0] + 0.5 * dt * dq[0],
q[1] + 0.5 * dt * dq[1],
q[2] + 0.5 * dt * dq[2],
q[3] + 0.5 * dt * dq[3],
];
quat_normalize(q_new)
}
#[derive(Debug, Clone)]
pub struct GpuRigidBody {
pub position: [f32; 3],
pub velocity: [f32; 3],
pub orientation: [f32; 4],
pub angular_velocity: [f32; 3],
pub mass: f32,
pub inv_inertia: [f32; 9],
}
impl GpuRigidBody {
pub fn new(mass: f32, ixx: f32, iyy: f32, izz: f32) -> Self {
let safe_inv = |v: f32| if v.abs() > 1e-12 { 1.0 / v } else { 0.0 };
let inv_inertia = [
safe_inv(ixx),
0.0,
0.0,
0.0,
safe_inv(iyy),
0.0,
0.0,
0.0,
safe_inv(izz),
];
Self {
position: [0.0; 3],
velocity: [0.0; 3],
orientation: [0.0, 0.0, 0.0, 1.0],
angular_velocity: [0.0; 3],
mass,
inv_inertia,
}
}
#[allow(dead_code)]
fn apply_inv_inertia(&self, v: [f32; 3]) -> [f32; 3] {
let i = &self.inv_inertia;
[
i[0] * v[0] + i[1] * v[1] + i[2] * v[2],
i[3] * v[0] + i[4] * v[1] + i[5] * v[2],
i[6] * v[0] + i[7] * v[1] + i[8] * v[2],
]
}
}
#[derive(Debug, Clone, Default)]
pub struct GpuRigidBodyBatch {
pub bodies: Vec<GpuRigidBody>,
}
impl GpuRigidBodyBatch {
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, body: GpuRigidBody) -> usize {
let idx = self.bodies.len();
self.bodies.push(body);
idx
}
pub fn integrate_all(&mut self, dt: f32, gravity: [f32; 3]) {
for body in &mut self.bodies {
body.velocity[0] += gravity[0] * dt;
body.velocity[1] += gravity[1] * dt;
body.velocity[2] += gravity[2] * dt;
body.position[0] += body.velocity[0] * dt;
body.position[1] += body.velocity[1] * dt;
body.position[2] += body.velocity[2] * dt;
let new_q = integrate_orientation(body.orientation, body.angular_velocity, dt);
body.orientation = new_q;
}
}
pub fn apply_impulse(&mut self, body_idx: usize, impulse: [f32; 3], point: [f32; 3]) {
let body = &mut self.bodies[body_idx];
let inv_mass = if body.mass > 1e-12 {
1.0 / body.mass
} else {
0.0
};
body.velocity[0] += impulse[0] * inv_mass;
body.velocity[1] += impulse[1] * inv_mass;
body.velocity[2] += impulse[2] * inv_mass;
let r = [
point[0] - body.position[0],
point[1] - body.position[1],
point[2] - body.position[2],
];
let torque_impulse = cross3f(r, impulse);
let delta_omega = apply_mat3(body.inv_inertia, torque_impulse);
body.angular_velocity[0] += delta_omega[0];
body.angular_velocity[1] += delta_omega[1];
body.angular_velocity[2] += delta_omega[2];
}
}
#[derive(Debug, Clone)]
pub struct BroadphasePairGpu {
pub body_a: usize,
pub body_b: usize,
pub aabb_a_center: [f32; 3],
pub aabb_a_half: [f32; 3],
pub aabb_b_center: [f32; 3],
pub aabb_b_half: [f32; 3],
}
#[derive(Debug, Clone, Default)]
pub struct GpuBroadphase {
pub bodies: Vec<GpuRigidBody>,
pub radii: Vec<f32>,
}
impl GpuBroadphase {
pub fn new() -> Self {
Self::default()
}
pub fn add_body(&mut self, body: GpuRigidBody, radius: f32) {
self.bodies.push(body);
self.radii.push(radius);
}
pub fn compute_pairs_sap(&self) -> Vec<BroadphasePairGpu> {
let n = self.bodies.len();
let mut pairs = Vec::new();
let mut order: Vec<usize> = (0..n).collect();
order.sort_by(|&a, &b| {
let ax = self.bodies[a].position[0] - self.radii[a];
let bx = self.bodies[b].position[0] - self.radii[b];
ax.partial_cmp(&bx).unwrap_or(std::cmp::Ordering::Equal)
});
for i in 0..order.len() {
let ai = order[i];
let a_max_x = self.bodies[ai].position[0] + self.radii[ai];
for j in (i + 1)..order.len() {
let bi = order[j];
let b_min_x = self.bodies[bi].position[0] - self.radii[bi];
if b_min_x > a_max_x {
break; }
if self.aabb_overlap(ai, bi) {
let ra = self.radii[ai];
let rb = self.radii[bi];
pairs.push(BroadphasePairGpu {
body_a: ai,
body_b: bi,
aabb_a_center: self.bodies[ai].position,
aabb_a_half: [ra, ra, ra],
aabb_b_center: self.bodies[bi].position,
aabb_b_half: [rb, rb, rb],
});
}
}
}
pairs
}
fn aabb_overlap(&self, a: usize, b: usize) -> bool {
for k in 0..3 {
let a_min = self.bodies[a].position[k] - self.radii[a];
let a_max = self.bodies[a].position[k] + self.radii[a];
let b_min = self.bodies[b].position[k] - self.radii[b];
let b_max = self.bodies[b].position[k] + self.radii[b];
if a_max < b_min || b_max < a_min {
return false;
}
}
true
}
}
#[derive(Debug, Clone)]
pub struct ContactManifoldGpu {
pub body_a: usize,
pub body_b: usize,
pub contact_points: Vec<[f32; 3]>,
pub normals: Vec<[f32; 3]>,
pub penetrations: Vec<f32>,
}
impl ContactManifoldGpu {
pub fn new(body_a: usize, body_b: usize) -> Self {
Self {
body_a,
body_b,
contact_points: Vec::new(),
normals: Vec::new(),
penetrations: Vec::new(),
}
}
pub fn add_contact(&mut self, point: [f32; 3], normal: [f32; 3], penetration: f32) {
self.contact_points.push(point);
self.normals.push(normal);
self.penetrations.push(penetration);
}
pub fn contact_count(&self) -> usize {
self.contact_points.len()
}
}
#[derive(Debug, Clone, Default)]
pub struct GpuConstraintSolver {
pub pairs: Vec<BroadphasePairGpu>,
pub manifolds: Vec<ContactManifoldGpu>,
}
impl GpuConstraintSolver {
pub fn new() -> Self {
Self::default()
}
pub fn add_manifold(&mut self, pair: BroadphasePairGpu, manifold: ContactManifoldGpu) {
self.pairs.push(pair);
self.manifolds.push(manifold);
}
#[allow(clippy::too_many_arguments)]
pub fn solve_sequential_impulse(
&self,
bodies: &mut [GpuRigidBody],
dt: f32,
iterations: usize,
) {
let _ = dt; let restitution = 0.3_f32;
for _ in 0..iterations {
for manifold in &self.manifolds {
let a = manifold.body_a;
let b = manifold.body_b;
for c in 0..manifold.contact_count() {
let n = manifold.normals[c];
let va = bodies[a].velocity;
let vb = bodies[b].velocity;
let rv = [va[0] - vb[0], va[1] - vb[1], va[2] - vb[2]];
let vn = dot3f(rv, n);
if vn >= 0.0 {
continue; }
let inv_ma = if bodies[a].mass > 1e-12 {
1.0 / bodies[a].mass
} else {
0.0
};
let inv_mb = if bodies[b].mass > 1e-12 {
1.0 / bodies[b].mass
} else {
0.0
};
let j = -(1.0 + restitution) * vn / (inv_ma + inv_mb);
bodies[a].velocity[0] += j * inv_ma * n[0];
bodies[a].velocity[1] += j * inv_ma * n[1];
bodies[a].velocity[2] += j * inv_ma * n[2];
bodies[b].velocity[0] -= j * inv_mb * n[0];
bodies[b].velocity[1] -= j * inv_mb * n[1];
bodies[b].velocity[2] -= j * inv_mb * n[2];
}
}
}
}
}
fn cross3f(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
}
fn dot3f(a: [f32; 3], b: [f32; 3]) -> f32 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
fn apply_mat3(m: [f32; 9], v: [f32; 3]) -> [f32; 3] {
[
m[0] * v[0] + m[1] * v[1] + m[2] * v[2],
m[3] * v[0] + m[4] * v[1] + m[5] * v[2],
m[6] * v[0] + m[7] * v[1] + m[8] * v[2],
]
}
#[cfg(test)]
mod tests {
use super::*;
const EPS: f32 = 1e-5;
fn approx_eq3(a: [f32; 3], b: [f32; 3]) -> bool {
(a[0] - b[0]).abs() < EPS && (a[1] - b[1]).abs() < EPS && (a[2] - b[2]).abs() < EPS
}
fn approx_eq4(a: [f32; 4], b: [f32; 4]) -> bool {
(a[0] - b[0]).abs() < EPS
&& (a[1] - b[1]).abs() < EPS
&& (a[2] - b[2]).abs() < EPS
&& (a[3] - b[3]).abs() < EPS
}
fn quat_norm(q: [f32; 4]) -> f32 {
(q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]).sqrt()
}
#[test]
fn test_quat_rotate_identity() {
let q = [0.0, 0.0, 0.0, 1.0_f32];
let v = [1.0, 2.0, 3.0_f32];
let r = quat_rotate(q, v);
assert!(approx_eq3(r, v), "identity quat should not rotate: {r:?}");
}
#[test]
fn test_quat_rotate_180_about_z() {
let q = [0.0, 0.0, 1.0_f32, 0.0];
let v = [1.0, 0.0, 0.0_f32];
let r = quat_rotate(q, v);
assert!(approx_eq3(r, [-1.0, 0.0, 0.0]), "180 Z rotate: {r:?}");
}
#[test]
fn test_quat_rotate_90_about_y() {
let half = std::f32::consts::FRAC_PI_4;
let q = [0.0, half.sin(), 0.0, half.cos()];
let v = [1.0, 0.0, 0.0_f32];
let r = quat_rotate(q, v);
assert!((r[0]).abs() < EPS, "x should be ~0: {}", r[0]);
assert!((r[1]).abs() < EPS, "y should be ~0: {}", r[1]);
assert!((r[2] + 1.0).abs() < EPS, "z should be ~-1: {}", r[2]);
}
#[test]
fn test_quat_rotate_preserves_length() {
let half = std::f32::consts::FRAC_PI_6;
let q = quat_normalize([half.sin(), 0.0, 0.0, half.cos()]);
let v = [3.0, 4.0, 0.0_f32];
let r = quat_rotate(q, v);
let len_v = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
let len_r = (r[0] * r[0] + r[1] * r[1] + r[2] * r[2]).sqrt();
assert!((len_v - len_r).abs() < EPS, "rotation must preserve length");
}
#[test]
fn test_quat_mul_identity() {
let id = [0.0, 0.0, 0.0, 1.0_f32];
let q = [0.1, 0.2, 0.3_f32, 0.9];
let q = quat_normalize(q);
assert!(approx_eq4(quat_mul(id, q), q));
assert!(approx_eq4(quat_mul(q, id), q));
}
#[test]
fn test_quat_mul_unit_norm() {
let a = quat_normalize([1.0, 0.0, 0.0_f32, 1.0]);
let b = quat_normalize([0.0, 1.0, 0.0_f32, 1.0]);
let c = quat_mul(a, b);
assert!(
(quat_norm(c) - 1.0).abs() < EPS,
"product must be unit: {}",
quat_norm(c)
);
}
#[test]
fn test_quat_mul_double_rotation() {
let half = std::f32::consts::FRAC_PI_4;
let q90 = [0.0, 0.0, half.sin(), half.cos()];
let q180 = quat_mul(q90, q90);
let v = [1.0, 0.0, 0.0_f32];
let r = quat_rotate(q180, v);
assert!((r[0] + 1.0).abs() < EPS, "should be [-1,0,0]: {r:?}");
}
#[test]
fn test_integrate_orientation_no_rotation() {
let q = [0.0, 0.0, 0.0, 1.0_f32];
let q2 = integrate_orientation(q, [0.0; 3], 0.01);
assert!((quat_norm(q2) - 1.0).abs() < EPS);
assert!(approx_eq4(q2, q));
}
#[test]
fn test_integrate_orientation_stays_unit() {
let q = [0.0, 0.0, 0.0, 1.0_f32];
let omega = [0.0, 0.0, 1.0_f32]; let mut q_cur = q;
for _ in 0..100 {
q_cur = integrate_orientation(q_cur, omega, 0.01);
}
assert!((quat_norm(q_cur) - 1.0).abs() < 1e-4);
}
#[test]
fn test_integrate_orientation_direction() {
let q = [0.0, 0.0, 0.0, 1.0_f32];
let omega = [1.0, 0.0, 0.0_f32];
let q2 = integrate_orientation(q, omega, 0.1);
assert!(
q2[0].abs() > 1e-4,
"qx should be > 0 after rotation: {}",
q2[0]
);
}
#[test]
fn test_quat_normalize_unit() {
let q = [1.0_f32, 0.0, 0.0, 0.0];
assert!(approx_eq4(quat_normalize(q), q));
}
#[test]
fn test_quat_normalize_zero_returns_identity() {
let q = quat_normalize([0.0; 4]);
assert!(approx_eq4(q, [0.0, 0.0, 0.0, 1.0]));
}
#[test]
fn test_quat_normalize_scales() {
let q = [2.0_f32, 0.0, 0.0, 0.0];
let n = quat_normalize(q);
assert!((n[0] - 1.0).abs() < EPS);
}
#[test]
fn test_rigid_body_new_defaults() {
let b = GpuRigidBody::new(1.0, 1.0, 1.0, 1.0);
assert_eq!(b.position, [0.0; 3]);
assert_eq!(b.velocity, [0.0; 3]);
assert_eq!(b.orientation, [0.0, 0.0, 0.0, 1.0]);
assert_eq!(b.angular_velocity, [0.0; 3]);
assert!((b.mass - 1.0).abs() < EPS);
}
#[test]
fn test_rigid_body_inv_inertia_diagonal() {
let b = GpuRigidBody::new(1.0, 2.0, 4.0, 8.0);
assert!((b.inv_inertia[0] - 0.5).abs() < EPS, "ixx");
assert!((b.inv_inertia[4] - 0.25).abs() < EPS, "iyy");
assert!((b.inv_inertia[8] - 0.125).abs() < EPS, "izz");
}
#[test]
fn test_batch_gravity_integration() {
let mut batch = GpuRigidBodyBatch::new();
batch.add(GpuRigidBody::new(1.0, 1.0, 1.0, 1.0));
batch.integrate_all(1.0, [0.0, -9.81, 0.0]);
assert!((batch.bodies[0].velocity[1] + 9.81).abs() < EPS);
assert!((batch.bodies[0].position[1] + 9.81).abs() < EPS);
}
#[test]
fn test_batch_no_gravity_no_motion() {
let mut batch = GpuRigidBodyBatch::new();
batch.add(GpuRigidBody::new(1.0, 1.0, 1.0, 1.0));
batch.integrate_all(1.0, [0.0; 3]);
assert_eq!(batch.bodies[0].position, [0.0; 3]);
assert_eq!(batch.bodies[0].velocity, [0.0; 3]);
}
#[test]
fn test_batch_multiple_bodies() {
let mut batch = GpuRigidBodyBatch::new();
for _ in 0..5 {
batch.add(GpuRigidBody::new(1.0, 1.0, 1.0, 1.0));
}
batch.integrate_all(0.5, [0.0, -9.81, 0.0]);
for b in &batch.bodies {
assert!((b.velocity[1] + 9.81 * 0.5).abs() < EPS);
}
}
#[test]
fn test_batch_add_returns_index() {
let mut batch = GpuRigidBodyBatch::new();
let i0 = batch.add(GpuRigidBody::new(1.0, 1.0, 1.0, 1.0));
let i1 = batch.add(GpuRigidBody::new(2.0, 1.0, 1.0, 1.0));
assert_eq!(i0, 0);
assert_eq!(i1, 1);
}
#[test]
fn test_apply_impulse_linear() {
let mut batch = GpuRigidBodyBatch::new();
batch.add(GpuRigidBody::new(2.0, 1.0, 1.0, 1.0));
batch.apply_impulse(0, [2.0, 0.0, 0.0], [0.0; 3]);
assert!((batch.bodies[0].velocity[0] - 1.0).abs() < EPS);
}
#[test]
fn test_apply_impulse_angular() {
let mut batch = GpuRigidBodyBatch::new();
let mut b = GpuRigidBody::new(1.0, 1.0, 1.0, 1.0);
b.position = [0.0; 3];
batch.add(b);
batch.apply_impulse(0, [0.0, 1.0, 0.0], [1.0, 0.0, 0.0]);
assert!(batch.bodies[0].angular_velocity[2].abs() > 1e-5);
}
#[test]
fn test_apply_impulse_zero_mass() {
let mut batch = GpuRigidBodyBatch::new();
batch.add(GpuRigidBody::new(0.0, 1.0, 1.0, 1.0));
batch.apply_impulse(0, [100.0, 0.0, 0.0], [0.0; 3]);
assert_eq!(batch.bodies[0].velocity, [0.0; 3]);
}
#[test]
fn test_broadphase_no_bodies() {
let bp = GpuBroadphase::new();
assert!(bp.compute_pairs_sap().is_empty());
}
#[test]
fn test_broadphase_two_overlapping() {
let mut bp = GpuBroadphase::new();
let mut b1 = GpuRigidBody::new(1.0, 1.0, 1.0, 1.0);
b1.position = [0.0; 3];
let mut b2 = GpuRigidBody::new(1.0, 1.0, 1.0, 1.0);
b2.position = [0.5, 0.0, 0.0];
bp.add_body(b1, 1.0);
bp.add_body(b2, 1.0);
let pairs = bp.compute_pairs_sap();
assert_eq!(pairs.len(), 1);
}
#[test]
fn test_broadphase_two_separated() {
let mut bp = GpuBroadphase::new();
let mut b1 = GpuRigidBody::new(1.0, 1.0, 1.0, 1.0);
b1.position = [0.0; 3];
let mut b2 = GpuRigidBody::new(1.0, 1.0, 1.0, 1.0);
b2.position = [100.0, 0.0, 0.0];
bp.add_body(b1, 0.5);
bp.add_body(b2, 0.5);
let pairs = bp.compute_pairs_sap();
assert!(pairs.is_empty());
}
#[test]
fn test_broadphase_three_bodies_two_pairs() {
let mut bp = GpuBroadphase::new();
for x in [0.0_f32, 1.0, 2.0] {
let mut b = GpuRigidBody::new(1.0, 1.0, 1.0, 1.0);
b.position = [x, 0.0, 0.0];
bp.add_body(b, 0.8);
}
let pairs = bp.compute_pairs_sap();
assert!(pairs.len() >= 2, "expected >= 2 pairs, got {}", pairs.len());
}
#[test]
fn test_broadphase_pair_indices_valid() {
let mut bp = GpuBroadphase::new();
let mut b1 = GpuRigidBody::new(1.0, 1.0, 1.0, 1.0);
b1.position = [0.0; 3];
let mut b2 = GpuRigidBody::new(1.0, 1.0, 1.0, 1.0);
b2.position = [0.3, 0.0, 0.0];
bp.add_body(b1, 0.5);
bp.add_body(b2, 0.5);
let pairs = bp.compute_pairs_sap();
assert_eq!(pairs.len(), 1);
let p = &pairs[0];
assert!(p.body_a < 2 && p.body_b < 2 && p.body_a != p.body_b);
}
#[test]
fn test_manifold_empty() {
let m = ContactManifoldGpu::new(0, 1);
assert_eq!(m.contact_count(), 0);
}
#[test]
fn test_manifold_add_contact() {
let mut m = ContactManifoldGpu::new(0, 1);
m.add_contact([0.0; 3], [0.0, 1.0, 0.0], 0.01);
assert_eq!(m.contact_count(), 1);
assert!((m.penetrations[0] - 0.01).abs() < EPS);
}
#[test]
fn test_manifold_multiple_contacts() {
let mut m = ContactManifoldGpu::new(0, 1);
for i in 0..4 {
m.add_contact([i as f32, 0.0, 0.0], [0.0, 1.0, 0.0], 0.01 * i as f32);
}
assert_eq!(m.contact_count(), 4);
}
#[test]
fn test_solver_no_manifolds() {
let solver = GpuConstraintSolver::new();
let mut bodies = vec![GpuRigidBody::new(1.0, 1.0, 1.0, 1.0)];
solver.solve_sequential_impulse(&mut bodies, 0.01, 10);
assert_eq!(bodies[0].velocity, [0.0; 3]);
}
#[test]
fn test_solver_separates_colliding_bodies() {
let mut b1 = GpuRigidBody::new(1.0, 1.0, 1.0, 1.0);
b1.velocity = [1.0, 0.0, 0.0];
let mut b2 = GpuRigidBody::new(1.0, 1.0, 1.0, 1.0);
b2.velocity = [-1.0, 0.0, 0.0];
b2.position = [0.5, 0.0, 0.0];
let mut solver = GpuConstraintSolver::new();
let pair = BroadphasePairGpu {
body_a: 0,
body_b: 1,
aabb_a_center: [0.0; 3],
aabb_a_half: [0.5; 3],
aabb_b_center: [0.5; 3],
aabb_b_half: [0.5; 3],
};
let mut manifold = ContactManifoldGpu::new(0, 1);
manifold.add_contact([0.25, 0.0, 0.0], [1.0, 0.0, 0.0], 0.01);
solver.add_manifold(pair, manifold);
let mut bodies = vec![b1, b2];
solver.solve_sequential_impulse(&mut bodies, 0.01, 10);
let rv_x = bodies[0].velocity[0] - bodies[1].velocity[0];
assert!(
rv_x >= -EPS,
"bodies should not penetrate further: rv_x={rv_x}"
);
}
#[test]
fn test_solver_static_body_kinematic() {
let mut b1 = GpuRigidBody::new(1.0, 1.0, 1.0, 1.0);
b1.velocity = [0.0, -1.0, 0.0];
let b2 = GpuRigidBody::new(0.0, 1.0, 1.0, 1.0);
let mut solver = GpuConstraintSolver::new();
let pair = BroadphasePairGpu {
body_a: 0,
body_b: 1,
aabb_a_center: [0.0; 3],
aabb_a_half: [0.5; 3],
aabb_b_center: [0.0, -0.9, 0.0],
aabb_b_half: [0.5; 3],
};
let mut manifold = ContactManifoldGpu::new(0, 1);
manifold.add_contact([0.0, -0.5, 0.0], [0.0, 1.0, 0.0], 0.1);
solver.add_manifold(pair, manifold);
let mut bodies = vec![b1, b2];
solver.solve_sequential_impulse(&mut bodies, 0.01, 10);
assert_eq!(bodies[1].velocity, [0.0; 3], "static body must not move");
assert!(
bodies[0].velocity[1] >= -EPS,
"dynamic body should bounce up"
);
}
#[test]
fn test_cross3f() {
let i = [1.0_f32, 0.0, 0.0];
let j = [0.0_f32, 1.0, 0.0];
let k = cross3f(i, j);
assert!(approx_eq3(k, [0.0, 0.0, 1.0]));
}
#[test]
fn test_dot3f() {
assert!((dot3f([1.0, 2.0, 3.0_f32], [4.0, 5.0, 6.0]) - 32.0).abs() < EPS);
}
#[test]
fn test_apply_mat3_identity() {
let id = [1.0_f32, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0];
let v = [3.0_f32, 4.0, 5.0];
assert!(approx_eq3(apply_mat3(id, v), v));
}
#[test]
fn test_apply_mat3_scale() {
let m = [2.0_f32, 0.0, 0.0, 0.0, 3.0, 0.0, 0.0, 0.0, 4.0];
let v = [1.0_f32, 1.0, 1.0];
assert!(approx_eq3(apply_mat3(m, v), [2.0, 3.0, 4.0]));
}
}