1pub fn quat_rotate(q: [f32; 4], v: [f32; 3]) -> [f32; 3] {
16 let (qx, qy, qz, qw) = (q[0], q[1], q[2], q[3]);
17 let tx = 2.0 * (qy * v[2] - qz * v[1]);
19 let ty = 2.0 * (qz * v[0] - qx * v[2]);
20 let tz = 2.0 * (qx * v[1] - qy * v[0]);
21 [
23 v[0] + qw * tx + (qy * tz - qz * ty),
24 v[1] + qw * ty + (qz * tx - qx * tz),
25 v[2] + qw * tz + (qx * ty - qy * tx),
26 ]
27}
28
29pub fn quat_mul(a: [f32; 4], b: [f32; 4]) -> [f32; 4] {
33 let (ax, ay, az, aw) = (a[0], a[1], a[2], a[3]);
34 let (bx, by, bz, bw) = (b[0], b[1], b[2], b[3]);
35 [
36 aw * bx + ax * bw + ay * bz - az * by,
37 aw * by - ax * bz + ay * bw + az * bx,
38 aw * bz + ax * by - ay * bx + az * bw,
39 aw * bw - ax * bx - ay * by - az * bz,
40 ]
41}
42
43pub fn quat_normalize(q: [f32; 4]) -> [f32; 4] {
47 let norm = (q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]).sqrt();
48 if norm < 1e-9 {
49 return [0.0, 0.0, 0.0, 1.0];
50 }
51 [q[0] / norm, q[1] / norm, q[2] / norm, q[3] / norm]
52}
53
54pub fn integrate_orientation(q: [f32; 4], omega: [f32; 3], dt: f32) -> [f32; 4] {
59 let omega_q = [omega[0], omega[1], omega[2], 0.0_f32];
61 let dq = quat_mul(omega_q, q);
62 let q_new = [
63 q[0] + 0.5 * dt * dq[0],
64 q[1] + 0.5 * dt * dq[1],
65 q[2] + 0.5 * dt * dq[2],
66 q[3] + 0.5 * dt * dq[3],
67 ];
68 quat_normalize(q_new)
69}
70
71#[derive(Debug, Clone)]
78pub struct GpuRigidBody {
79 pub position: [f32; 3],
81 pub velocity: [f32; 3],
83 pub orientation: [f32; 4],
85 pub angular_velocity: [f32; 3],
87 pub mass: f32,
89 pub inv_inertia: [f32; 9],
91}
92
93impl GpuRigidBody {
94 pub fn new(mass: f32, ixx: f32, iyy: f32, izz: f32) -> Self {
98 let safe_inv = |v: f32| if v.abs() > 1e-12 { 1.0 / v } else { 0.0 };
99 let inv_inertia = [
100 safe_inv(ixx),
101 0.0,
102 0.0,
103 0.0,
104 safe_inv(iyy),
105 0.0,
106 0.0,
107 0.0,
108 safe_inv(izz),
109 ];
110 Self {
111 position: [0.0; 3],
112 velocity: [0.0; 3],
113 orientation: [0.0, 0.0, 0.0, 1.0],
114 angular_velocity: [0.0; 3],
115 mass,
116 inv_inertia,
117 }
118 }
119}
120
121#[derive(Debug, Clone, Default)]
125pub struct GpuRigidBodyBatch {
126 pub bodies: Vec<GpuRigidBody>,
128}
129
130impl GpuRigidBodyBatch {
131 pub fn new() -> Self {
133 Self::default()
134 }
135
136 pub fn add(&mut self, body: GpuRigidBody) -> usize {
138 let idx = self.bodies.len();
139 self.bodies.push(body);
140 idx
141 }
142
143 pub fn integrate_all(&mut self, dt: f32, gravity: [f32; 3]) {
148 for body in &mut self.bodies {
149 body.velocity[0] += gravity[0] * dt;
151 body.velocity[1] += gravity[1] * dt;
152 body.velocity[2] += gravity[2] * dt;
153 body.position[0] += body.velocity[0] * dt;
154 body.position[1] += body.velocity[1] * dt;
155 body.position[2] += body.velocity[2] * dt;
156 let new_q = integrate_orientation(body.orientation, body.angular_velocity, dt);
158 body.orientation = new_q;
159 }
160 }
161
162 pub fn apply_impulse(&mut self, body_idx: usize, impulse: [f32; 3], point: [f32; 3]) {
167 let body = &mut self.bodies[body_idx];
168 let inv_mass = if body.mass > 1e-12 {
169 1.0 / body.mass
170 } else {
171 0.0
172 };
173 body.velocity[0] += impulse[0] * inv_mass;
175 body.velocity[1] += impulse[1] * inv_mass;
176 body.velocity[2] += impulse[2] * inv_mass;
177 let r = [
179 point[0] - body.position[0],
180 point[1] - body.position[1],
181 point[2] - body.position[2],
182 ];
183 let torque_impulse = cross3f(r, impulse);
185 let delta_omega = apply_mat3(body.inv_inertia, torque_impulse);
186 body.angular_velocity[0] += delta_omega[0];
187 body.angular_velocity[1] += delta_omega[1];
188 body.angular_velocity[2] += delta_omega[2];
189 }
190}
191
192#[derive(Debug, Clone)]
196pub struct BroadphasePairGpu {
197 pub body_a: usize,
199 pub body_b: usize,
201 pub aabb_a_center: [f32; 3],
203 pub aabb_a_half: [f32; 3],
205 pub aabb_b_center: [f32; 3],
207 pub aabb_b_half: [f32; 3],
209}
210
211#[derive(Debug, Clone, Default)]
217pub struct GpuBroadphase {
218 pub bodies: Vec<GpuRigidBody>,
220 pub radii: Vec<f32>,
222}
223
224impl GpuBroadphase {
225 pub fn new() -> Self {
227 Self::default()
228 }
229
230 pub fn add_body(&mut self, body: GpuRigidBody, radius: f32) {
232 self.bodies.push(body);
233 self.radii.push(radius);
234 }
235
236 pub fn compute_pairs_sap(&self) -> Vec<BroadphasePairGpu> {
240 let n = self.bodies.len();
241 let mut pairs = Vec::new();
242
243 let mut order: Vec<usize> = (0..n).collect();
245 order.sort_by(|&a, &b| {
246 let ax = self.bodies[a].position[0] - self.radii[a];
247 let bx = self.bodies[b].position[0] - self.radii[b];
248 ax.partial_cmp(&bx).unwrap_or(std::cmp::Ordering::Equal)
249 });
250
251 for (i, &ai) in order.iter().enumerate() {
252 let a_max_x = self.bodies[ai].position[0] + self.radii[ai];
253 for &bi in order.iter().skip(i + 1) {
254 let b_min_x = self.bodies[bi].position[0] - self.radii[bi];
255 if b_min_x > a_max_x {
256 break; }
258 if self.aabb_overlap(ai, bi) {
260 let ra = self.radii[ai];
261 let rb = self.radii[bi];
262 pairs.push(BroadphasePairGpu {
263 body_a: ai,
264 body_b: bi,
265 aabb_a_center: self.bodies[ai].position,
266 aabb_a_half: [ra, ra, ra],
267 aabb_b_center: self.bodies[bi].position,
268 aabb_b_half: [rb, rb, rb],
269 });
270 }
271 }
272 }
273 pairs
274 }
275
276 fn aabb_overlap(&self, a: usize, b: usize) -> bool {
278 for k in 0..3 {
279 let a_min = self.bodies[a].position[k] - self.radii[a];
280 let a_max = self.bodies[a].position[k] + self.radii[a];
281 let b_min = self.bodies[b].position[k] - self.radii[b];
282 let b_max = self.bodies[b].position[k] + self.radii[b];
283 if a_max < b_min || b_max < a_min {
284 return false;
285 }
286 }
287 true
288 }
289}
290
291#[derive(Debug, Clone)]
295pub struct ContactManifoldGpu {
296 pub body_a: usize,
298 pub body_b: usize,
300 pub contact_points: Vec<[f32; 3]>,
302 pub normals: Vec<[f32; 3]>,
304 pub penetrations: Vec<f32>,
306}
307
308impl ContactManifoldGpu {
309 pub fn new(body_a: usize, body_b: usize) -> Self {
311 Self {
312 body_a,
313 body_b,
314 contact_points: Vec::new(),
315 normals: Vec::new(),
316 penetrations: Vec::new(),
317 }
318 }
319
320 pub fn add_contact(&mut self, point: [f32; 3], normal: [f32; 3], penetration: f32) {
322 self.contact_points.push(point);
323 self.normals.push(normal);
324 self.penetrations.push(penetration);
325 }
326
327 pub fn contact_count(&self) -> usize {
329 self.contact_points.len()
330 }
331}
332
333#[derive(Debug, Clone, Default)]
337pub struct GpuConstraintSolver {
338 pub pairs: Vec<BroadphasePairGpu>,
340 pub manifolds: Vec<ContactManifoldGpu>,
342}
343
344impl GpuConstraintSolver {
345 pub fn new() -> Self {
347 Self::default()
348 }
349
350 pub fn add_manifold(&mut self, pair: BroadphasePairGpu, manifold: ContactManifoldGpu) {
352 self.pairs.push(pair);
353 self.manifolds.push(manifold);
354 }
355
356 pub fn solve_sequential_impulse(
361 &self,
362 bodies: &mut [GpuRigidBody],
363 dt: f32,
364 iterations: usize,
365 ) {
366 let _ = dt; let restitution = 0.3_f32;
368 for _ in 0..iterations {
369 for manifold in &self.manifolds {
370 let a = manifold.body_a;
371 let b = manifold.body_b;
372 for c in 0..manifold.contact_count() {
373 let n = manifold.normals[c];
374 let va = bodies[a].velocity;
376 let vb = bodies[b].velocity;
377 let rv = [va[0] - vb[0], va[1] - vb[1], va[2] - vb[2]];
378 let vn = dot3f(rv, n);
379 if vn >= 0.0 {
380 continue; }
382 let inv_ma = if bodies[a].mass > 1e-12 {
383 1.0 / bodies[a].mass
384 } else {
385 0.0
386 };
387 let inv_mb = if bodies[b].mass > 1e-12 {
388 1.0 / bodies[b].mass
389 } else {
390 0.0
391 };
392 let j = -(1.0 + restitution) * vn / (inv_ma + inv_mb);
393 bodies[a].velocity[0] += j * inv_ma * n[0];
395 bodies[a].velocity[1] += j * inv_ma * n[1];
396 bodies[a].velocity[2] += j * inv_ma * n[2];
397 bodies[b].velocity[0] -= j * inv_mb * n[0];
398 bodies[b].velocity[1] -= j * inv_mb * n[1];
399 bodies[b].velocity[2] -= j * inv_mb * n[2];
400 }
401 }
402 }
403 }
404}
405
406fn cross3f(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
410 [
411 a[1] * b[2] - a[2] * b[1],
412 a[2] * b[0] - a[0] * b[2],
413 a[0] * b[1] - a[1] * b[0],
414 ]
415}
416
417fn dot3f(a: [f32; 3], b: [f32; 3]) -> f32 {
419 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
420}
421
422fn apply_mat3(m: [f32; 9], v: [f32; 3]) -> [f32; 3] {
424 [
425 m[0] * v[0] + m[1] * v[1] + m[2] * v[2],
426 m[3] * v[0] + m[4] * v[1] + m[5] * v[2],
427 m[6] * v[0] + m[7] * v[1] + m[8] * v[2],
428 ]
429}
430
431#[cfg(test)]
434mod tests {
435 use super::*;
436
437 const EPS: f32 = 1e-5;
438
439 fn approx_eq3(a: [f32; 3], b: [f32; 3]) -> bool {
440 (a[0] - b[0]).abs() < EPS && (a[1] - b[1]).abs() < EPS && (a[2] - b[2]).abs() < EPS
441 }
442
443 fn approx_eq4(a: [f32; 4], b: [f32; 4]) -> bool {
444 (a[0] - b[0]).abs() < EPS
445 && (a[1] - b[1]).abs() < EPS
446 && (a[2] - b[2]).abs() < EPS
447 && (a[3] - b[3]).abs() < EPS
448 }
449
450 fn quat_norm(q: [f32; 4]) -> f32 {
451 (q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]).sqrt()
452 }
453
454 #[test]
457 fn test_quat_rotate_identity() {
458 let q = [0.0, 0.0, 0.0, 1.0_f32];
459 let v = [1.0, 2.0, 3.0_f32];
460 let r = quat_rotate(q, v);
461 assert!(approx_eq3(r, v), "identity quat should not rotate: {r:?}");
462 }
463
464 #[test]
465 fn test_quat_rotate_180_about_z() {
466 let q = [0.0, 0.0, 1.0_f32, 0.0];
468 let v = [1.0, 0.0, 0.0_f32];
469 let r = quat_rotate(q, v);
470 assert!(approx_eq3(r, [-1.0, 0.0, 0.0]), "180 Z rotate: {r:?}");
471 }
472
473 #[test]
474 fn test_quat_rotate_90_about_y() {
475 let half = std::f32::consts::FRAC_PI_4;
477 let q = [0.0, half.sin(), 0.0, half.cos()];
478 let v = [1.0, 0.0, 0.0_f32];
479 let r = quat_rotate(q, v);
480 assert!((r[0]).abs() < EPS, "x should be ~0: {}", r[0]);
482 assert!((r[1]).abs() < EPS, "y should be ~0: {}", r[1]);
483 assert!((r[2] + 1.0).abs() < EPS, "z should be ~-1: {}", r[2]);
484 }
485
486 #[test]
487 fn test_quat_rotate_preserves_length() {
488 let half = std::f32::consts::FRAC_PI_6;
489 let q = quat_normalize([half.sin(), 0.0, 0.0, half.cos()]);
490 let v = [3.0, 4.0, 0.0_f32];
491 let r = quat_rotate(q, v);
492 let len_v = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
493 let len_r = (r[0] * r[0] + r[1] * r[1] + r[2] * r[2]).sqrt();
494 assert!((len_v - len_r).abs() < EPS, "rotation must preserve length");
495 }
496
497 #[test]
500 fn test_quat_mul_identity() {
501 let id = [0.0, 0.0, 0.0, 1.0_f32];
502 let q = [0.1, 0.2, 0.3_f32, 0.9];
503 let q = quat_normalize(q);
504 assert!(approx_eq4(quat_mul(id, q), q));
505 assert!(approx_eq4(quat_mul(q, id), q));
506 }
507
508 #[test]
509 fn test_quat_mul_unit_norm() {
510 let a = quat_normalize([1.0, 0.0, 0.0_f32, 1.0]);
511 let b = quat_normalize([0.0, 1.0, 0.0_f32, 1.0]);
512 let c = quat_mul(a, b);
513 assert!(
514 (quat_norm(c) - 1.0).abs() < EPS,
515 "product must be unit: {}",
516 quat_norm(c)
517 );
518 }
519
520 #[test]
521 fn test_quat_mul_double_rotation() {
522 let half = std::f32::consts::FRAC_PI_4;
524 let q90 = [0.0, 0.0, half.sin(), half.cos()];
525 let q180 = quat_mul(q90, q90);
526 let v = [1.0, 0.0, 0.0_f32];
527 let r = quat_rotate(q180, v);
528 assert!((r[0] + 1.0).abs() < EPS, "should be [-1,0,0]: {r:?}");
529 }
530
531 #[test]
534 fn test_integrate_orientation_no_rotation() {
535 let q = [0.0, 0.0, 0.0, 1.0_f32];
536 let q2 = integrate_orientation(q, [0.0; 3], 0.01);
537 assert!((quat_norm(q2) - 1.0).abs() < EPS);
538 assert!(approx_eq4(q2, q));
539 }
540
541 #[test]
542 fn test_integrate_orientation_stays_unit() {
543 let q = [0.0, 0.0, 0.0, 1.0_f32];
544 let omega = [0.0, 0.0, 1.0_f32]; let mut q_cur = q;
546 for _ in 0..100 {
547 q_cur = integrate_orientation(q_cur, omega, 0.01);
548 }
549 assert!((quat_norm(q_cur) - 1.0).abs() < 1e-4);
550 }
551
552 #[test]
553 fn test_integrate_orientation_direction() {
554 let q = [0.0, 0.0, 0.0, 1.0_f32];
556 let omega = [1.0, 0.0, 0.0_f32];
557 let q2 = integrate_orientation(q, omega, 0.1);
558 assert!(
560 q2[0].abs() > 1e-4,
561 "qx should be > 0 after rotation: {}",
562 q2[0]
563 );
564 }
565
566 #[test]
569 fn test_quat_normalize_unit() {
570 let q = [1.0_f32, 0.0, 0.0, 0.0];
571 assert!(approx_eq4(quat_normalize(q), q));
572 }
573
574 #[test]
575 fn test_quat_normalize_zero_returns_identity() {
576 let q = quat_normalize([0.0; 4]);
577 assert!(approx_eq4(q, [0.0, 0.0, 0.0, 1.0]));
578 }
579
580 #[test]
581 fn test_quat_normalize_scales() {
582 let q = [2.0_f32, 0.0, 0.0, 0.0];
583 let n = quat_normalize(q);
584 assert!((n[0] - 1.0).abs() < EPS);
585 }
586
587 #[test]
590 fn test_rigid_body_new_defaults() {
591 let b = GpuRigidBody::new(1.0, 1.0, 1.0, 1.0);
592 assert_eq!(b.position, [0.0; 3]);
593 assert_eq!(b.velocity, [0.0; 3]);
594 assert_eq!(b.orientation, [0.0, 0.0, 0.0, 1.0]);
595 assert_eq!(b.angular_velocity, [0.0; 3]);
596 assert!((b.mass - 1.0).abs() < EPS);
597 }
598
599 #[test]
600 fn test_rigid_body_inv_inertia_diagonal() {
601 let b = GpuRigidBody::new(1.0, 2.0, 4.0, 8.0);
602 assert!((b.inv_inertia[0] - 0.5).abs() < EPS, "ixx");
603 assert!((b.inv_inertia[4] - 0.25).abs() < EPS, "iyy");
604 assert!((b.inv_inertia[8] - 0.125).abs() < EPS, "izz");
605 }
606
607 #[test]
610 fn test_batch_gravity_integration() {
611 let mut batch = GpuRigidBodyBatch::new();
612 batch.add(GpuRigidBody::new(1.0, 1.0, 1.0, 1.0));
613 batch.integrate_all(1.0, [0.0, -9.81, 0.0]);
614 assert!((batch.bodies[0].velocity[1] + 9.81).abs() < EPS);
615 assert!((batch.bodies[0].position[1] + 9.81).abs() < EPS);
616 }
617
618 #[test]
619 fn test_batch_no_gravity_no_motion() {
620 let mut batch = GpuRigidBodyBatch::new();
621 batch.add(GpuRigidBody::new(1.0, 1.0, 1.0, 1.0));
622 batch.integrate_all(1.0, [0.0; 3]);
623 assert_eq!(batch.bodies[0].position, [0.0; 3]);
624 assert_eq!(batch.bodies[0].velocity, [0.0; 3]);
625 }
626
627 #[test]
628 fn test_batch_multiple_bodies() {
629 let mut batch = GpuRigidBodyBatch::new();
630 for _ in 0..5 {
631 batch.add(GpuRigidBody::new(1.0, 1.0, 1.0, 1.0));
632 }
633 batch.integrate_all(0.5, [0.0, -9.81, 0.0]);
634 for b in &batch.bodies {
635 assert!((b.velocity[1] + 9.81 * 0.5).abs() < EPS);
636 }
637 }
638
639 #[test]
640 fn test_batch_add_returns_index() {
641 let mut batch = GpuRigidBodyBatch::new();
642 let i0 = batch.add(GpuRigidBody::new(1.0, 1.0, 1.0, 1.0));
643 let i1 = batch.add(GpuRigidBody::new(2.0, 1.0, 1.0, 1.0));
644 assert_eq!(i0, 0);
645 assert_eq!(i1, 1);
646 }
647
648 #[test]
651 fn test_apply_impulse_linear() {
652 let mut batch = GpuRigidBodyBatch::new();
653 batch.add(GpuRigidBody::new(2.0, 1.0, 1.0, 1.0));
654 batch.apply_impulse(0, [2.0, 0.0, 0.0], [0.0; 3]);
656 assert!((batch.bodies[0].velocity[0] - 1.0).abs() < EPS);
657 }
658
659 #[test]
660 fn test_apply_impulse_angular() {
661 let mut batch = GpuRigidBodyBatch::new();
662 let mut b = GpuRigidBody::new(1.0, 1.0, 1.0, 1.0);
663 b.position = [0.0; 3];
664 batch.add(b);
665 batch.apply_impulse(0, [0.0, 1.0, 0.0], [1.0, 0.0, 0.0]);
667 assert!(batch.bodies[0].angular_velocity[2].abs() > 1e-5);
668 }
669
670 #[test]
671 fn test_apply_impulse_zero_mass() {
672 let mut batch = GpuRigidBodyBatch::new();
673 batch.add(GpuRigidBody::new(0.0, 1.0, 1.0, 1.0));
674 batch.apply_impulse(0, [100.0, 0.0, 0.0], [0.0; 3]);
675 assert_eq!(batch.bodies[0].velocity, [0.0; 3]);
676 }
677
678 #[test]
681 fn test_broadphase_no_bodies() {
682 let bp = GpuBroadphase::new();
683 assert!(bp.compute_pairs_sap().is_empty());
684 }
685
686 #[test]
687 fn test_broadphase_two_overlapping() {
688 let mut bp = GpuBroadphase::new();
689 let mut b1 = GpuRigidBody::new(1.0, 1.0, 1.0, 1.0);
690 b1.position = [0.0; 3];
691 let mut b2 = GpuRigidBody::new(1.0, 1.0, 1.0, 1.0);
692 b2.position = [0.5, 0.0, 0.0];
693 bp.add_body(b1, 1.0);
694 bp.add_body(b2, 1.0);
695 let pairs = bp.compute_pairs_sap();
696 assert_eq!(pairs.len(), 1);
697 }
698
699 #[test]
700 fn test_broadphase_two_separated() {
701 let mut bp = GpuBroadphase::new();
702 let mut b1 = GpuRigidBody::new(1.0, 1.0, 1.0, 1.0);
703 b1.position = [0.0; 3];
704 let mut b2 = GpuRigidBody::new(1.0, 1.0, 1.0, 1.0);
705 b2.position = [100.0, 0.0, 0.0];
706 bp.add_body(b1, 0.5);
707 bp.add_body(b2, 0.5);
708 let pairs = bp.compute_pairs_sap();
709 assert!(pairs.is_empty());
710 }
711
712 #[test]
713 fn test_broadphase_three_bodies_two_pairs() {
714 let mut bp = GpuBroadphase::new();
715 for x in [0.0_f32, 1.0, 2.0] {
716 let mut b = GpuRigidBody::new(1.0, 1.0, 1.0, 1.0);
717 b.position = [x, 0.0, 0.0];
718 bp.add_body(b, 0.8);
719 }
720 let pairs = bp.compute_pairs_sap();
721 assert!(pairs.len() >= 2, "expected >= 2 pairs, got {}", pairs.len());
723 }
724
725 #[test]
726 fn test_broadphase_pair_indices_valid() {
727 let mut bp = GpuBroadphase::new();
728 let mut b1 = GpuRigidBody::new(1.0, 1.0, 1.0, 1.0);
729 b1.position = [0.0; 3];
730 let mut b2 = GpuRigidBody::new(1.0, 1.0, 1.0, 1.0);
731 b2.position = [0.3, 0.0, 0.0];
732 bp.add_body(b1, 0.5);
733 bp.add_body(b2, 0.5);
734 let pairs = bp.compute_pairs_sap();
735 assert_eq!(pairs.len(), 1);
736 let p = &pairs[0];
737 assert!(p.body_a < 2 && p.body_b < 2 && p.body_a != p.body_b);
738 }
739
740 #[test]
743 fn test_manifold_empty() {
744 let m = ContactManifoldGpu::new(0, 1);
745 assert_eq!(m.contact_count(), 0);
746 }
747
748 #[test]
749 fn test_manifold_add_contact() {
750 let mut m = ContactManifoldGpu::new(0, 1);
751 m.add_contact([0.0; 3], [0.0, 1.0, 0.0], 0.01);
752 assert_eq!(m.contact_count(), 1);
753 assert!((m.penetrations[0] - 0.01).abs() < EPS);
754 }
755
756 #[test]
757 fn test_manifold_multiple_contacts() {
758 let mut m = ContactManifoldGpu::new(0, 1);
759 for i in 0..4 {
760 m.add_contact([i as f32, 0.0, 0.0], [0.0, 1.0, 0.0], 0.01 * i as f32);
761 }
762 assert_eq!(m.contact_count(), 4);
763 }
764
765 #[test]
768 fn test_solver_no_manifolds() {
769 let solver = GpuConstraintSolver::new();
770 let mut bodies = vec![GpuRigidBody::new(1.0, 1.0, 1.0, 1.0)];
771 solver.solve_sequential_impulse(&mut bodies, 0.01, 10);
772 assert_eq!(bodies[0].velocity, [0.0; 3]);
773 }
774
775 #[test]
776 fn test_solver_separates_colliding_bodies() {
777 let mut b1 = GpuRigidBody::new(1.0, 1.0, 1.0, 1.0);
778 b1.velocity = [1.0, 0.0, 0.0];
779 let mut b2 = GpuRigidBody::new(1.0, 1.0, 1.0, 1.0);
780 b2.velocity = [-1.0, 0.0, 0.0];
781 b2.position = [0.5, 0.0, 0.0];
782
783 let mut solver = GpuConstraintSolver::new();
784 let pair = BroadphasePairGpu {
785 body_a: 0,
786 body_b: 1,
787 aabb_a_center: [0.0; 3],
788 aabb_a_half: [0.5; 3],
789 aabb_b_center: [0.5; 3],
790 aabb_b_half: [0.5; 3],
791 };
792 let mut manifold = ContactManifoldGpu::new(0, 1);
793 manifold.add_contact([0.25, 0.0, 0.0], [1.0, 0.0, 0.0], 0.01);
794 solver.add_manifold(pair, manifold);
795
796 let mut bodies = vec![b1, b2];
797 solver.solve_sequential_impulse(&mut bodies, 0.01, 10);
798
799 let rv_x = bodies[0].velocity[0] - bodies[1].velocity[0];
801 assert!(
802 rv_x >= -EPS,
803 "bodies should not penetrate further: rv_x={rv_x}"
804 );
805 }
806
807 #[test]
808 fn test_solver_static_body_kinematic() {
809 let mut b1 = GpuRigidBody::new(1.0, 1.0, 1.0, 1.0);
811 b1.velocity = [0.0, -1.0, 0.0];
812 let b2 = GpuRigidBody::new(0.0, 1.0, 1.0, 1.0); let mut solver = GpuConstraintSolver::new();
815 let pair = BroadphasePairGpu {
816 body_a: 0,
817 body_b: 1,
818 aabb_a_center: [0.0; 3],
819 aabb_a_half: [0.5; 3],
820 aabb_b_center: [0.0, -0.9, 0.0],
821 aabb_b_half: [0.5; 3],
822 };
823 let mut manifold = ContactManifoldGpu::new(0, 1);
824 manifold.add_contact([0.0, -0.5, 0.0], [0.0, 1.0, 0.0], 0.1);
825 solver.add_manifold(pair, manifold);
826
827 let mut bodies = vec![b1, b2];
828 solver.solve_sequential_impulse(&mut bodies, 0.01, 10);
829
830 assert_eq!(bodies[1].velocity, [0.0; 3], "static body must not move");
831 assert!(
832 bodies[0].velocity[1] >= -EPS,
833 "dynamic body should bounce up"
834 );
835 }
836
837 #[test]
840 fn test_cross3f() {
841 let i = [1.0_f32, 0.0, 0.0];
842 let j = [0.0_f32, 1.0, 0.0];
843 let k = cross3f(i, j);
844 assert!(approx_eq3(k, [0.0, 0.0, 1.0]));
845 }
846
847 #[test]
848 fn test_dot3f() {
849 assert!((dot3f([1.0, 2.0, 3.0_f32], [4.0, 5.0, 6.0]) - 32.0).abs() < EPS);
850 }
851
852 #[test]
853 fn test_apply_mat3_identity() {
854 let id = [1.0_f32, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0];
855 let v = [3.0_f32, 4.0, 5.0];
856 assert!(approx_eq3(apply_mat3(id, v), v));
857 }
858
859 #[test]
860 fn test_apply_mat3_scale() {
861 let m = [2.0_f32, 0.0, 0.0, 0.0, 3.0, 0.0, 0.0, 0.0, 4.0];
862 let v = [1.0_f32, 1.0, 1.0];
863 assert!(approx_eq3(apply_mat3(m, v), [2.0, 3.0, 4.0]));
864 }
865}