1use nalgebra::{Matrix3, UnitQuaternion, Vector3};
37
38#[cfg(not(feature = "std"))]
39use micromath::F32Ext;
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub struct BodyId(usize);
44
45#[derive(Debug, Clone, Copy, PartialEq)]
47pub enum BodyType {
48 Dynamic,
50 Static,
53}
54
55#[derive(Debug, Clone, Copy, PartialEq)]
60pub enum Collider {
61 Sphere { radius: f32 },
63 Aabb { half_extents: Vector3<f32> },
65 Capsule { height: f32, radius: f32 },
69}
70
71#[derive(Debug, Clone)]
75pub struct Contact {
76 pub body_a: BodyId,
78 pub body_b: BodyId,
80 pub normal: Vector3<f32>,
82 pub penetration: f32,
84}
85
86#[derive(Debug, Clone)]
91pub struct RigidBody {
92 pub position: Vector3<f32>,
94 pub velocity: Vector3<f32>,
95 pub mass: f32,
96 pub inv_mass: f32,
97 pub body_type: BodyType,
98 pub restitution: f32,
99 pub collider: Option<Collider>,
100
101 pub friction: f32,
105
106 force_accumulator: Vector3<f32>,
108
109 pub damping: f32,
112
113 pub orientation: UnitQuaternion<f32>,
116
117 pub angular_velocity: Vector3<f32>,
119
120 pub inv_inertia_local: Matrix3<f32>,
123
124 torque_accumulator: Vector3<f32>,
126
127 pub angular_damping: f32,
130
131 pub active: bool,
134}
135
136impl RigidBody {
137 pub fn new(mass: f32) -> Self {
146 assert!(
147 mass > 0.0 && mass.is_finite(),
148 "mass must be positive and finite"
149 );
150 let i = 0.4 * mass;
152 let inv_i = 1.0 / i;
153 Self {
154 position: Vector3::zeros(),
155 velocity: Vector3::zeros(),
156 mass,
157 inv_mass: 1.0 / mass,
158 body_type: BodyType::Dynamic,
159 restitution: 0.5,
160 collider: None,
161 friction: 0.3,
162 force_accumulator: Vector3::zeros(),
163 damping: 0.01,
164 orientation: UnitQuaternion::identity(),
165 angular_velocity: Vector3::zeros(),
166 inv_inertia_local: Matrix3::from_diagonal(&Vector3::new(inv_i, inv_i, inv_i)),
167 torque_accumulator: Vector3::zeros(),
168 angular_damping: 0.01,
169 active: true,
170 }
171 }
172
173 pub fn new_static() -> Self {
175 Self {
176 position: Vector3::zeros(),
177 velocity: Vector3::zeros(),
178 mass: f32::INFINITY,
179 inv_mass: 0.0,
180 body_type: BodyType::Static,
181 restitution: 0.5,
182 collider: None,
183 friction: 0.5,
184 force_accumulator: Vector3::zeros(),
185 damping: 0.0,
186 orientation: UnitQuaternion::identity(),
187 angular_velocity: Vector3::zeros(),
188 inv_inertia_local: Matrix3::zeros(),
189 torque_accumulator: Vector3::zeros(),
190 angular_damping: 0.0,
191 active: true,
192 }
193 }
194
195 pub fn with_position(mut self, position: Vector3<f32>) -> Self {
197 self.position = position;
198 self
199 }
200
201 pub fn with_velocity(mut self, velocity: Vector3<f32>) -> Self {
203 self.velocity = velocity;
204 self
205 }
206
207 pub fn with_restitution(mut self, restitution: f32) -> Self {
209 self.restitution = restitution.clamp(0.0, 1.0);
210 self
211 }
212
213 pub fn with_damping(mut self, damping: f32) -> Self {
215 self.damping = damping.clamp(0.0, 1.0);
216 self
217 }
218
219 pub fn with_collider(mut self, collider: Collider) -> Self {
221 self.collider = Some(collider);
222 self
223 }
224
225 pub fn with_friction(mut self, friction: f32) -> Self {
227 self.friction = friction.clamp(0.0, 1.0);
228 self
229 }
230
231 pub fn with_angular_velocity(mut self, angular_velocity: Vector3<f32>) -> Self {
233 self.angular_velocity = angular_velocity;
234 self
235 }
236
237 pub fn with_angular_damping(mut self, damping: f32) -> Self {
239 self.angular_damping = damping.clamp(0.0, 1.0);
240 self
241 }
242
243 pub fn with_inertia_sphere(mut self, radius: f32) -> Self {
247 if self.body_type == BodyType::Dynamic {
248 let i = 0.4 * self.mass * radius * radius;
249 let inv_i = 1.0 / i;
250 self.inv_inertia_local = Matrix3::from_diagonal(&Vector3::new(inv_i, inv_i, inv_i));
251 }
252 self
253 }
254
255 pub fn with_inertia_box(mut self, half_extents: Vector3<f32>) -> Self {
262 if self.body_type == BodyType::Dynamic {
263 let hx2 = 4.0 * half_extents.x * half_extents.x;
264 let hy2 = 4.0 * half_extents.y * half_extents.y;
265 let hz2 = 4.0 * half_extents.z * half_extents.z;
266 let k = self.mass / 12.0;
267 let ix = k * (hy2 + hz2);
268 let iy = k * (hx2 + hz2);
269 let iz = k * (hx2 + hy2);
270 self.inv_inertia_local =
271 Matrix3::from_diagonal(&Vector3::new(1.0 / ix, 1.0 / iy, 1.0 / iz));
272 }
273 self
274 }
275
276 #[inline]
278 pub fn apply_force(&mut self, force: Vector3<f32>) {
279 self.force_accumulator += force;
280 }
281
282 #[inline]
285 pub fn apply_impulse(&mut self, impulse: Vector3<f32>) {
286 if self.body_type == BodyType::Dynamic {
287 self.velocity += impulse * self.inv_mass;
288 }
289 }
290
291 #[inline]
293 pub fn apply_torque(&mut self, torque: Vector3<f32>) {
294 self.torque_accumulator += torque;
295 }
296
297 #[inline]
300 pub fn apply_angular_impulse(&mut self, impulse: Vector3<f32>) {
301 if self.body_type == BodyType::Dynamic {
302 let inv_inertia_world = self.inv_inertia_world();
303 self.angular_velocity += inv_inertia_world * impulse;
304 }
305 }
306
307 #[inline]
310 pub fn inv_inertia_world(&self) -> Matrix3<f32> {
311 let r = self.orientation.to_rotation_matrix();
312 r.matrix() * self.inv_inertia_local * r.matrix().transpose()
313 }
314
315 #[inline]
317 pub fn speed(&self) -> f32 {
318 self.velocity.norm()
319 }
320
321 #[inline]
323 pub fn kinetic_energy(&self) -> f32 {
324 0.5 * self.mass * self.velocity.norm_squared()
325 }
326
327 fn integrate(&mut self, dt: f32, gravity: Vector3<f32>) {
332 if self.body_type != BodyType::Dynamic {
333 self.force_accumulator = Vector3::zeros();
334 self.torque_accumulator = Vector3::zeros();
335 return;
336 }
337
338 let acceleration = gravity + self.force_accumulator * self.inv_mass;
340 self.velocity += acceleration * dt;
341 self.velocity *= 1.0 - self.damping;
342 self.position += self.velocity * dt;
343
344 let inv_inertia_world = self.inv_inertia_world();
346 let angular_acceleration = inv_inertia_world * self.torque_accumulator;
347 self.angular_velocity += angular_acceleration * dt;
348 self.angular_velocity *= 1.0 - self.angular_damping;
349
350 let w = &self.angular_velocity;
353 let half_dt = 0.5 * dt;
354 let dq = nalgebra::Quaternion::new(0.0, w.x * half_dt, w.y * half_dt, w.z * half_dt);
355 let q = self.orientation.into_inner();
356 let new_q = q + dq * q;
357 self.orientation = UnitQuaternion::new_normalize(new_q);
359
360 self.force_accumulator = Vector3::zeros();
362 self.torque_accumulator = Vector3::zeros();
363 }
364}
365
366fn collide(
375 pos_a: &Vector3<f32>,
376 col_a: &Collider,
377 pos_b: &Vector3<f32>,
378 col_b: &Collider,
379) -> Option<(Vector3<f32>, f32)> {
380 match (col_a, col_b) {
381 (Collider::Sphere { radius: ra }, Collider::Sphere { radius: rb }) => {
382 collide_sphere_sphere(pos_a, *ra, pos_b, *rb)
383 }
384 (Collider::Aabb { half_extents: ha }, Collider::Aabb { half_extents: hb }) => {
385 collide_aabb_aabb(pos_a, ha, pos_b, hb)
386 }
387 (Collider::Sphere { radius }, Collider::Aabb { half_extents }) => {
388 collide_sphere_aabb(pos_a, *radius, pos_b, half_extents)
389 }
390 (Collider::Aabb { half_extents }, Collider::Sphere { radius }) => {
391 let result = collide_sphere_aabb(pos_b, *radius, pos_a, half_extents);
393 result.map(|(normal, pen)| (-normal, pen))
394 }
395 (
396 Collider::Capsule {
397 height: ha,
398 radius: ra,
399 },
400 Collider::Capsule {
401 height: hb,
402 radius: rb,
403 },
404 ) => collide_capsule_capsule(pos_a, *ha, *ra, pos_b, *hb, *rb),
405 (
406 Collider::Sphere { radius },
407 Collider::Capsule {
408 height,
409 radius: cap_radius,
410 },
411 ) => collide_sphere_capsule(pos_a, *radius, pos_b, *height, *cap_radius),
412 (
413 Collider::Capsule {
414 height,
415 radius: cap_radius,
416 },
417 Collider::Sphere { radius },
418 ) => {
419 let result = collide_sphere_capsule(pos_b, *radius, pos_a, *height, *cap_radius);
420 result.map(|(normal, pen)| (-normal, pen))
421 }
422 (Collider::Capsule { height, radius }, Collider::Aabb { half_extents }) => {
423 collide_capsule_aabb(pos_a, *height, *radius, pos_b, half_extents)
424 }
425 (Collider::Aabb { half_extents }, Collider::Capsule { height, radius }) => {
426 let result = collide_capsule_aabb(pos_b, *height, *radius, pos_a, half_extents);
427 result.map(|(normal, pen)| (-normal, pen))
428 }
429 }
430}
431
432fn collide_sphere_sphere(
436 pos_a: &Vector3<f32>,
437 radius_a: f32,
438 pos_b: &Vector3<f32>,
439 radius_b: f32,
440) -> Option<(Vector3<f32>, f32)> {
441 let diff = pos_b - pos_a;
442 let dist_sq = diff.norm_squared();
443 let sum_r = radius_a + radius_b;
444
445 if dist_sq >= sum_r * sum_r {
446 return None;
447 }
448
449 let dist = dist_sq.sqrt();
450 let penetration = sum_r - dist;
451
452 let normal = if dist > 1e-6 {
453 diff / dist
454 } else {
455 Vector3::new(0.0, 1.0, 0.0)
457 };
458
459 Some((normal, penetration))
460}
461
462fn collide_aabb_aabb(
466 pos_a: &Vector3<f32>,
467 half_a: &Vector3<f32>,
468 pos_b: &Vector3<f32>,
469 half_b: &Vector3<f32>,
470) -> Option<(Vector3<f32>, f32)> {
471 let diff = pos_b - pos_a;
472
473 let overlap_x = half_a.x + half_b.x - diff.x.abs();
474 if overlap_x <= 0.0 {
475 return None;
476 }
477 let overlap_y = half_a.y + half_b.y - diff.y.abs();
478 if overlap_y <= 0.0 {
479 return None;
480 }
481 let overlap_z = half_a.z + half_b.z - diff.z.abs();
482 if overlap_z <= 0.0 {
483 return None;
484 }
485
486 if overlap_x <= overlap_y && overlap_x <= overlap_z {
488 let sign = if diff.x >= 0.0 { 1.0 } else { -1.0 };
489 Some((Vector3::new(sign, 0.0, 0.0), overlap_x))
490 } else if overlap_y <= overlap_z {
491 let sign = if diff.y >= 0.0 { 1.0 } else { -1.0 };
492 Some((Vector3::new(0.0, sign, 0.0), overlap_y))
493 } else {
494 let sign = if diff.z >= 0.0 { 1.0 } else { -1.0 };
495 Some((Vector3::new(0.0, 0.0, sign), overlap_z))
496 }
497}
498
499fn collide_sphere_aabb(
504 sphere_pos: &Vector3<f32>,
505 sphere_radius: f32,
506 aabb_pos: &Vector3<f32>,
507 aabb_half: &Vector3<f32>,
508) -> Option<(Vector3<f32>, f32)> {
509 let aabb_min = aabb_pos - aabb_half;
511 let aabb_max = aabb_pos + aabb_half;
512
513 let closest = Vector3::new(
514 sphere_pos.x.clamp(aabb_min.x, aabb_max.x),
515 sphere_pos.y.clamp(aabb_min.y, aabb_max.y),
516 sphere_pos.z.clamp(aabb_min.z, aabb_max.z),
517 );
518
519 let diff = sphere_pos - closest;
520 let dist_sq = diff.norm_squared();
521
522 if dist_sq >= sphere_radius * sphere_radius {
523 return None;
524 }
525
526 let dist = dist_sq.sqrt();
527
528 if dist > 1e-6 {
529 let normal = -diff / dist;
532 let penetration = sphere_radius - dist;
533 Some((normal, penetration))
534 } else {
535 let dx_pos = aabb_max.x - sphere_pos.x;
537 let dx_neg = sphere_pos.x - aabb_min.x;
538 let dy_pos = aabb_max.y - sphere_pos.y;
539 let dy_neg = sphere_pos.y - aabb_min.y;
540 let dz_pos = aabb_max.z - sphere_pos.z;
541 let dz_neg = sphere_pos.z - aabb_min.z;
542
543 let mut min_dist = dx_pos;
544 let mut normal = Vector3::new(1.0, 0.0, 0.0);
545
546 if dx_neg < min_dist {
547 min_dist = dx_neg;
548 normal = Vector3::new(-1.0, 0.0, 0.0);
549 }
550 if dy_pos < min_dist {
551 min_dist = dy_pos;
552 normal = Vector3::new(0.0, 1.0, 0.0);
553 }
554 if dy_neg < min_dist {
555 min_dist = dy_neg;
556 normal = Vector3::new(0.0, -1.0, 0.0);
557 }
558 if dz_pos < min_dist {
559 min_dist = dz_pos;
560 normal = Vector3::new(0.0, 0.0, 1.0);
561 }
562 if dz_neg < min_dist {
563 min_dist = dz_neg;
564 normal = Vector3::new(0.0, 0.0, -1.0);
565 }
566
567 let penetration = sphere_radius + min_dist;
568 Some((normal, penetration))
569 }
570}
571
572fn collide_capsule_capsule(
576 pos_a: &Vector3<f32>,
577 height_a: f32,
578 radius_a: f32,
579 pos_b: &Vector3<f32>,
580 height_b: f32,
581 radius_b: f32,
582) -> Option<(Vector3<f32>, f32)> {
583 let half_height_a = height_a * 0.5;
585 let half_height_b = height_b * 0.5;
586
587 let a_bottom = *pos_a + Vector3::new(0.0, -half_height_a, 0.0);
588 let a_top = *pos_a + Vector3::new(0.0, half_height_a, 0.0);
589 let b_bottom = *pos_b + Vector3::new(0.0, -half_height_b, 0.0);
590 let b_top = *pos_b + Vector3::new(0.0, half_height_b, 0.0);
591
592 let (closest_a, closest_b) = closest_points_on_segments(a_bottom, a_top, b_bottom, b_top);
594
595 collide_sphere_sphere(&closest_a, radius_a, &closest_b, radius_b)
597}
598
599fn collide_sphere_capsule(
601 sphere_pos: &Vector3<f32>,
602 sphere_radius: f32,
603 capsule_pos: &Vector3<f32>,
604 capsule_height: f32,
605 capsule_radius: f32,
606) -> Option<(Vector3<f32>, f32)> {
607 let half_height = capsule_height * 0.5;
609 let cap_bottom = *capsule_pos + Vector3::new(0.0, -half_height, 0.0);
610 let cap_top = *capsule_pos + Vector3::new(0.0, half_height, 0.0);
611
612 let closest = closest_point_on_segment(*sphere_pos, cap_bottom, cap_top);
614
615 collide_sphere_sphere(sphere_pos, sphere_radius, &closest, capsule_radius)
617}
618
619fn collide_capsule_aabb(
621 capsule_pos: &Vector3<f32>,
622 capsule_height: f32,
623 capsule_radius: f32,
624 aabb_pos: &Vector3<f32>,
625 aabb_half_extents: &Vector3<f32>,
626) -> Option<(Vector3<f32>, f32)> {
627 let half_height = capsule_height * 0.5;
629 let cap_bottom = *capsule_pos + Vector3::new(0.0, -half_height, 0.0);
630 let cap_top = *capsule_pos + Vector3::new(0.0, half_height, 0.0);
631
632 let aabb_min = aabb_pos - aabb_half_extents;
634 let aabb_max = aabb_pos + aabb_half_extents;
635
636 let closest_on_capsule =
638 closest_point_on_segment_to_aabb(cap_bottom, cap_top, &aabb_min, &aabb_max);
639
640 collide_sphere_aabb(
642 &closest_on_capsule,
643 capsule_radius,
644 aabb_pos,
645 aabb_half_extents,
646 )
647}
648
649fn closest_point_on_segment(
651 point: Vector3<f32>,
652 seg_start: Vector3<f32>,
653 seg_end: Vector3<f32>,
654) -> Vector3<f32> {
655 let segment = seg_end - seg_start;
656 let segment_len_sq = segment.norm_squared();
657
658 if segment_len_sq < 1e-6 {
659 return seg_start;
660 }
661
662 let t = ((point - seg_start).dot(&segment) / segment_len_sq).clamp(0.0, 1.0);
663 seg_start + segment * t
664}
665
666fn closest_points_on_segments(
668 a_start: Vector3<f32>,
669 a_end: Vector3<f32>,
670 b_start: Vector3<f32>,
671 b_end: Vector3<f32>,
672) -> (Vector3<f32>, Vector3<f32>) {
673 let d1 = a_end - a_start;
674 let d2 = b_end - b_start;
675 let r = a_start - b_start;
676
677 let a = d1.dot(&d1);
678 let e = d2.dot(&d2);
679 let f = d2.dot(&r);
680
681 let epsilon = 1e-6;
682
683 if a < epsilon && e < epsilon {
684 return (a_start, b_start);
686 }
687
688 let mut s: f32;
689 let mut t: f32;
690
691 if a < epsilon {
692 s = 0.0;
694 t = (f / e).clamp(0.0, 1.0);
695 } else {
696 let c = d1.dot(&r);
697 if e < epsilon {
698 t = 0.0;
700 s = (-c / a).clamp(0.0, 1.0);
701 } else {
702 let b = d1.dot(&d2);
704 let denom = a * e - b * b;
705
706 s = if denom.abs() > epsilon {
707 ((b * f - c * e) / denom).clamp(0.0, 1.0)
708 } else {
709 0.0
710 };
711
712 t = (b * s + f) / e;
713
714 if t < 0.0 {
715 t = 0.0;
716 s = (-c / a).clamp(0.0, 1.0);
717 } else if t > 1.0 {
718 t = 1.0;
719 s = ((b - c) / a).clamp(0.0, 1.0);
720 }
721 }
722 }
723
724 (a_start + d1 * s, b_start + d2 * t)
725}
726
727fn closest_point_on_segment_to_aabb(
729 seg_start: Vector3<f32>,
730 seg_end: Vector3<f32>,
731 aabb_min: &Vector3<f32>,
732 aabb_max: &Vector3<f32>,
733) -> Vector3<f32> {
734 let aabb_center = (aabb_min + aabb_max) * 0.5;
736 closest_point_on_segment(aabb_center, seg_start, seg_end)
737}
738
739#[derive(Debug, Clone, Copy)]
745pub struct Ray {
746 pub origin: Vector3<f32>,
748 pub direction: Vector3<f32>,
750}
751
752impl Ray {
753 pub fn new(origin: Vector3<f32>, direction: Vector3<f32>) -> Self {
757 Self {
758 origin,
759 direction: direction.normalize(),
760 }
761 }
762
763 pub fn point_at(&self, t: f32) -> Vector3<f32> {
765 self.origin + self.direction * t
766 }
767}
768
769#[derive(Debug, Clone, Copy)]
771pub struct RayCastHit {
772 pub body_id: BodyId,
774 pub point: Vector3<f32>,
776 pub normal: Vector3<f32>,
778 pub distance: f32,
780 pub uv: Option<[f32; 2]>,
782}
783
784fn ray_intersect_sphere(ray: &Ray, center: &Vector3<f32>, radius: f32) -> Option<f32> {
788 let oc = ray.origin - center;
789 let a = ray.direction.dot(&ray.direction);
790 let b = 2.0 * oc.dot(&ray.direction);
791 let c = oc.dot(&oc) - radius * radius;
792 let discriminant = b * b - 4.0 * a * c;
793
794 if discriminant < 0.0 {
795 return None;
796 }
797
798 let sqrt_disc = discriminant.sqrt();
799 let t1 = (-b - sqrt_disc) / (2.0 * a);
800 let t2 = (-b + sqrt_disc) / (2.0 * a);
801
802 if t1 > 0.0 {
804 Some(t1)
805 } else if t2 > 0.0 {
806 Some(t2)
807 } else {
808 None
809 }
810}
811
812fn ray_intersect_aabb(
816 ray: &Ray,
817 aabb_pos: &Vector3<f32>,
818 half_extents: &Vector3<f32>,
819) -> Option<f32> {
820 let aabb_min = aabb_pos - half_extents;
821 let aabb_max = aabb_pos + half_extents;
822
823 let mut tmin = (aabb_min.x - ray.origin.x) / ray.direction.x;
824 let mut tmax = (aabb_max.x - ray.origin.x) / ray.direction.x;
825
826 if tmin > tmax {
827 core::mem::swap(&mut tmin, &mut tmax);
828 }
829
830 let mut tymin = (aabb_min.y - ray.origin.y) / ray.direction.y;
831 let mut tymax = (aabb_max.y - ray.origin.y) / ray.direction.y;
832
833 if tymin > tymax {
834 core::mem::swap(&mut tymin, &mut tymax);
835 }
836
837 if (tmin > tymax) || (tymin > tmax) {
838 return None;
839 }
840
841 tmin = tmin.max(tymin);
842 tmax = tmax.min(tymax);
843
844 let mut tzmin = (aabb_min.z - ray.origin.z) / ray.direction.z;
845 let mut tzmax = (aabb_max.z - ray.origin.z) / ray.direction.z;
846
847 if tzmin > tzmax {
848 core::mem::swap(&mut tzmin, &mut tzmax);
849 }
850
851 if (tmin > tzmax) || (tzmin > tmax) {
852 return None;
853 }
854
855 tmin = tmin.max(tzmin);
856
857 if tmin > 0.0 {
858 Some(tmin)
859 } else if tmax > 0.0 {
860 Some(tmax)
861 } else {
862 None
863 }
864}
865
866fn ray_intersect_capsule(
870 ray: &Ray,
871 capsule_pos: &Vector3<f32>,
872 height: f32,
873 radius: f32,
874) -> Option<f32> {
875 let half_height = height * 0.5;
877 let cap_bottom = *capsule_pos + Vector3::new(0.0, -half_height, 0.0);
878 let cap_top = *capsule_pos + Vector3::new(0.0, half_height, 0.0);
879
880 let mut min_t = f32::MAX;
883 let mut hit = false;
884
885 if let Some(t) = ray_intersect_sphere(ray, &cap_bottom, radius) {
887 min_t = min_t.min(t);
888 hit = true;
889 }
890
891 if let Some(t) = ray_intersect_sphere(ray, &cap_top, radius) {
893 min_t = min_t.min(t);
894 hit = true;
895 }
896
897 if hit { Some(min_t) } else { None }
901}
902
903#[derive(Debug, Clone, Copy, PartialEq, Eq)]
909pub struct ConstraintId(usize);
910
911#[derive(Debug, Clone)]
917pub enum Constraint {
918 Distance {
923 body_a: BodyId,
924 body_b: BodyId,
925 anchor_a: Vector3<f32>,
927 anchor_b: Vector3<f32>,
929 rest_length: f32,
931 compliance: f32,
933 },
934 BallSocket {
939 body_a: BodyId,
940 body_b: BodyId,
941 anchor_a: Vector3<f32>,
943 anchor_b: Vector3<f32>,
945 compliance: f32,
947 },
948 Fixed {
952 body_a: BodyId,
953 body_b: BodyId,
954 anchor_a: Vector3<f32>,
956 anchor_b: Vector3<f32>,
958 relative_orientation: UnitQuaternion<f32>,
960 compliance: f32,
962 },
963}
964
965pub struct PhysicsWorld<const N: usize, const M: usize = 0> {
989 bodies: heapless::Vec<RigidBody, N>,
990 constraints: heapless::Vec<Constraint, M>,
991 gravity: Vector3<f32>,
992 pub solver_iterations: u32,
994}
995
996impl<const N: usize, const M: usize> PhysicsWorld<N, M> {
997 pub fn new() -> Self {
999 Self {
1000 bodies: heapless::Vec::new(),
1001 constraints: heapless::Vec::new(),
1002 gravity: Vector3::zeros(),
1003 solver_iterations: 4,
1004 }
1005 }
1006
1007 pub fn set_gravity(&mut self, gravity: Vector3<f32>) {
1009 self.gravity = gravity;
1010 }
1011
1012 pub fn gravity(&self) -> Vector3<f32> {
1014 self.gravity
1015 }
1016
1017 pub fn add_body(&mut self, body: RigidBody) -> Option<BodyId> {
1019 let id = BodyId(self.bodies.len());
1020 self.bodies.push(body).ok()?;
1021 Some(id)
1022 }
1023
1024 pub fn body(&self, id: BodyId) -> Option<&RigidBody> {
1026 self.bodies.get(id.0)
1027 }
1028
1029 pub fn body_mut(&mut self, id: BodyId) -> Option<&mut RigidBody> {
1031 self.bodies.get_mut(id.0)
1032 }
1033
1034 pub fn body_count(&self) -> usize {
1036 self.bodies.len()
1037 }
1038
1039 pub fn active_body_count(&self) -> usize {
1041 self.bodies.iter().filter(|b| b.active).count()
1042 }
1043
1044 pub fn remove_body(&mut self, id: BodyId) -> bool {
1053 if let Some(body) = self.bodies.get_mut(id.0) {
1054 if body.active {
1055 body.active = false;
1056 body.velocity = Vector3::zeros();
1057 body.angular_velocity = Vector3::zeros();
1058 body.force_accumulator = Vector3::zeros();
1059 body.torque_accumulator = Vector3::zeros();
1060 return true;
1061 }
1062 }
1063 false
1064 }
1065
1066 pub fn set_active(&mut self, id: BodyId, active: bool) -> bool {
1071 if let Some(body) = self.bodies.get_mut(id.0) {
1072 body.active = active;
1073 true
1074 } else {
1075 false
1076 }
1077 }
1078
1079 pub fn bodies(&self) -> impl Iterator<Item = (BodyId, &RigidBody)> {
1081 self.bodies.iter().enumerate().map(|(i, b)| (BodyId(i), b))
1082 }
1083
1084 pub fn bodies_mut(&mut self) -> impl Iterator<Item = (BodyId, &mut RigidBody)> {
1086 self.bodies
1087 .iter_mut()
1088 .enumerate()
1089 .map(|(i, b)| (BodyId(i), b))
1090 }
1091
1092 pub fn ray_cast(&self, ray: &Ray, max_distance: f32) -> Option<RayCastHit> {
1118 let mut nearest_hit: Option<RayCastHit> = None;
1119 let mut min_distance = max_distance;
1120
1121 for (i, body) in self.bodies.iter().enumerate() {
1122 if !body.active {
1123 continue;
1124 }
1125
1126 let collider = match &body.collider {
1127 Some(c) => c,
1128 None => continue,
1129 };
1130
1131 let distance = match collider {
1132 Collider::Sphere { radius } => ray_intersect_sphere(ray, &body.position, *radius),
1133 Collider::Aabb { half_extents } => {
1134 ray_intersect_aabb(ray, &body.position, half_extents)
1135 }
1136 Collider::Capsule { height, radius } => {
1137 ray_intersect_capsule(ray, &body.position, *height, *radius)
1138 }
1139 };
1140
1141 if let Some(dist) = distance {
1142 if dist < min_distance {
1143 let point = ray.point_at(dist);
1144 let normal = match collider {
1145 Collider::Sphere { .. } => (point - body.position).normalize(),
1146 Collider::Aabb { .. } => {
1147 (point - body.position).normalize()
1149 }
1150 Collider::Capsule { height, .. } => {
1151 let half_height = height * 0.5;
1153 let cap_bottom = body.position + Vector3::new(0.0, -half_height, 0.0);
1154 let cap_top = body.position + Vector3::new(0.0, half_height, 0.0);
1155 let closest = closest_point_on_segment(point, cap_bottom, cap_top);
1156 (point - closest).normalize()
1157 }
1158 };
1159
1160 let uv = match collider {
1161 Collider::Sphere { .. } => {
1162 let theta = normal.z.atan2(normal.x); let phi = normal.y.clamp(-1.0, 1.0).asin(); let u = theta / (2.0 * core::f32::consts::PI) + 0.5;
1166 let v = 0.5 - phi / core::f32::consts::PI;
1167 Some([u, v])
1168 }
1169 Collider::Aabb { half_extents } => {
1170 let abs_n = [normal.x.abs(), normal.y.abs(), normal.z.abs()];
1172 let (u, v) = if abs_n[0] >= abs_n[1] && abs_n[0] >= abs_n[2] {
1173 (
1175 0.5 + (point.y - body.position.y) / (2.0 * half_extents.y),
1176 0.5 + (point.z - body.position.z) / (2.0 * half_extents.z),
1177 )
1178 } else if abs_n[1] >= abs_n[0] && abs_n[1] >= abs_n[2] {
1179 (
1181 0.5 + (point.x - body.position.x) / (2.0 * half_extents.x),
1182 0.5 + (point.z - body.position.z) / (2.0 * half_extents.z),
1183 )
1184 } else {
1185 (
1187 0.5 + (point.x - body.position.x) / (2.0 * half_extents.x),
1188 0.5 + (point.y - body.position.y) / (2.0 * half_extents.y),
1189 )
1190 };
1191 Some([u.clamp(0.0, 1.0), v.clamp(0.0, 1.0)])
1192 }
1193 Collider::Capsule { height, .. } => {
1194 let half_height = height * 0.5;
1196 let local_y =
1197 (point.y - body.position.y).clamp(-half_height, half_height);
1198 let theta = normal.z.atan2(normal.x);
1199 let u = theta / (2.0 * core::f32::consts::PI) + 0.5;
1200 let v = (local_y + half_height) / height;
1201 Some([u, v])
1202 }
1203 };
1204
1205 min_distance = dist;
1206 nearest_hit = Some(RayCastHit {
1207 body_id: BodyId(i),
1208 point,
1209 normal,
1210 distance: dist,
1211 uv,
1212 });
1213 }
1214 }
1215 }
1216
1217 nearest_hit
1218 }
1219
1220 pub fn add_constraint(&mut self, constraint: Constraint) -> Option<ConstraintId> {
1225 let id = ConstraintId(self.constraints.len());
1226 self.constraints.push(constraint).ok()?;
1227 Some(id)
1228 }
1229
1230 pub fn constraint(&self, id: ConstraintId) -> Option<&Constraint> {
1232 self.constraints.get(id.0)
1233 }
1234
1235 pub fn constraint_mut(&mut self, id: ConstraintId) -> Option<&mut Constraint> {
1237 self.constraints.get_mut(id.0)
1238 }
1239
1240 pub fn remove_constraint(&mut self, id: ConstraintId) -> bool {
1245 if id.0 < self.constraints.len() {
1246 self.constraints.swap_remove(id.0);
1247 true
1248 } else {
1249 false
1250 }
1251 }
1252
1253 pub fn constraint_count(&self) -> usize {
1255 self.constraints.len()
1256 }
1257
1258 pub fn add_distance_constraint(
1263 &mut self,
1264 body_a: BodyId,
1265 anchor_a: Vector3<f32>,
1266 body_b: BodyId,
1267 anchor_b: Vector3<f32>,
1268 compliance: f32,
1269 ) -> Option<ConstraintId> {
1270 let world_a = self.body_to_world(body_a, &anchor_a)?;
1271 let world_b = self.body_to_world(body_b, &anchor_b)?;
1272 let rest_length = (world_b - world_a).norm();
1273
1274 self.add_constraint(Constraint::Distance {
1275 body_a,
1276 body_b,
1277 anchor_a,
1278 anchor_b,
1279 rest_length,
1280 compliance,
1281 })
1282 }
1283
1284 pub fn add_ball_socket(
1289 &mut self,
1290 body_a: BodyId,
1291 anchor_a: Vector3<f32>,
1292 body_b: BodyId,
1293 anchor_b: Vector3<f32>,
1294 compliance: f32,
1295 ) -> Option<ConstraintId> {
1296 self.add_constraint(Constraint::BallSocket {
1297 body_a,
1298 body_b,
1299 anchor_a,
1300 anchor_b,
1301 compliance,
1302 })
1303 }
1304
1305 pub fn add_fixed_joint(
1309 &mut self,
1310 body_a: BodyId,
1311 anchor_a: Vector3<f32>,
1312 body_b: BodyId,
1313 anchor_b: Vector3<f32>,
1314 compliance: f32,
1315 ) -> Option<ConstraintId> {
1316 let qa = self.bodies.get(body_a.0)?.orientation;
1317 let qb = self.bodies.get(body_b.0)?.orientation;
1318 let relative_orientation = qb * qa.inverse();
1319
1320 self.add_constraint(Constraint::Fixed {
1321 body_a,
1322 body_b,
1323 anchor_a,
1324 anchor_b,
1325 relative_orientation,
1326 compliance,
1327 })
1328 }
1329
1330 fn body_to_world(&self, id: BodyId, local_point: &Vector3<f32>) -> Option<Vector3<f32>> {
1332 let body = self.bodies.get(id.0)?;
1333 Some(body.position + body.orientation * local_point)
1334 }
1335
1336 #[inline]
1338 fn body_x_extent(body: &RigidBody, collider: &Collider) -> (f32, f32) {
1339 let px = body.position.x;
1340 match collider {
1341 Collider::Sphere { radius } => (px - radius, px + radius),
1342 Collider::Aabb { half_extents } => (px - half_extents.x, px + half_extents.x),
1343 Collider::Capsule { radius, .. } => (px - radius, px + radius),
1344 }
1345 }
1346
1347 pub fn detect_collisions<const C: usize>(&self) -> heapless::Vec<Contact, C> {
1359 let mut contacts = heapless::Vec::new();
1360
1361 let mut entries = heapless::Vec::<(f32, f32, usize), N>::new();
1363 for (i, body) in self.bodies.iter().enumerate() {
1364 if !body.active {
1365 continue;
1366 }
1367 if let Some(ref col) = body.collider {
1368 let (min_x, max_x) = Self::body_x_extent(body, col);
1369 let _ = entries.push((min_x, max_x, i));
1370 }
1371 }
1372
1373 for i in 1..entries.len() {
1375 let mut j = i;
1376 while j > 0 && entries[j].0 < entries[j - 1].0 {
1377 entries.swap(j, j - 1);
1378 j -= 1;
1379 }
1380 }
1381
1382 let len = entries.len();
1384 for i in 0..len {
1385 let (_, max_x_i, idx_a) = entries[i];
1386 let body_a = &self.bodies[idx_a];
1387 let col_a = body_a.collider.as_ref().unwrap();
1388
1389 for j in (i + 1)..len {
1390 let (min_x_j, _, idx_b) = entries[j];
1391
1392 if min_x_j > max_x_i {
1395 break;
1396 }
1397
1398 let body_b = &self.bodies[idx_b];
1399 let col_b = body_b.collider.as_ref().unwrap();
1400
1401 if body_a.body_type == BodyType::Static && body_b.body_type == BodyType::Static {
1403 continue;
1404 }
1405
1406 if let Some((normal, penetration)) =
1407 collide(&body_a.position, col_a, &body_b.position, col_b)
1408 {
1409 let _ = contacts.push(Contact {
1410 body_a: BodyId(idx_a),
1411 body_b: BodyId(idx_b),
1412 normal,
1413 penetration,
1414 });
1415 }
1416 }
1417 }
1418
1419 contacts
1420 }
1421
1422 pub fn resolve_contacts(&mut self, contacts: &[Contact]) {
1430 for contact in contacts {
1431 let a = contact.body_a.0;
1432 let b = contact.body_b.0;
1433
1434 let inv_mass_a = self.bodies[a].inv_mass;
1435 let inv_mass_b = self.bodies[b].inv_mass;
1436
1437 let contact_point =
1440 self.bodies[a].position + contact.normal * (contact.penetration * 0.5);
1441
1442 let ra = contact_point - self.bodies[a].position;
1443 let rb = contact_point - self.bodies[b].position;
1444
1445 let inv_inertia_a = self.bodies[a].inv_inertia_world();
1446 let inv_inertia_b = self.bodies[b].inv_inertia_world();
1447
1448 let inv_mass_sum = inv_mass_a + inv_mass_b;
1450 if inv_mass_sum == 0.0 {
1451 continue; }
1453 let correction = contact.normal * (contact.penetration / inv_mass_sum);
1454 self.bodies[a].position -= correction * inv_mass_a;
1455 self.bodies[b].position += correction * inv_mass_b;
1456
1457 let vel_a = self.bodies[a].velocity + self.bodies[a].angular_velocity.cross(&ra);
1459 let vel_b = self.bodies[b].velocity + self.bodies[b].angular_velocity.cross(&rb);
1460 let relative_vel = vel_b - vel_a;
1461 let vel_along_normal = relative_vel.dot(&contact.normal);
1462
1463 if vel_along_normal >= 0.0 {
1465 continue;
1466 }
1467
1468 let restitution = self.bodies[a].restitution.min(self.bodies[b].restitution);
1469
1470 let ra_cross_n = ra.cross(&contact.normal);
1473 let rb_cross_n = rb.cross(&contact.normal);
1474 let angular_term_a = (inv_inertia_a * ra_cross_n).cross(&ra).dot(&contact.normal);
1475 let angular_term_b = (inv_inertia_b * rb_cross_n).cross(&rb).dot(&contact.normal);
1476 let eff_mass_inv = inv_mass_a + inv_mass_b + angular_term_a + angular_term_b;
1477
1478 if eff_mass_inv <= 0.0 {
1479 continue;
1480 }
1481
1482 let j = -(1.0 + restitution) * vel_along_normal / eff_mass_inv;
1484
1485 let impulse = contact.normal * j;
1486 self.bodies[a].velocity -= impulse * inv_mass_a;
1487 self.bodies[b].velocity += impulse * inv_mass_b;
1488 self.bodies[a].angular_velocity -= inv_inertia_a * ra.cross(&impulse);
1489 self.bodies[b].angular_velocity += inv_inertia_b * rb.cross(&impulse);
1490
1491 let mu_a = self.bodies[a].friction;
1493 let mu_b = self.bodies[b].friction;
1494 let mu = (mu_a * mu_b).sqrt();
1495
1496 if mu > 1e-6 {
1497 let vel_a = self.bodies[a].velocity + self.bodies[a].angular_velocity.cross(&ra);
1499 let vel_b = self.bodies[b].velocity + self.bodies[b].angular_velocity.cross(&rb);
1500 let relative_vel = vel_b - vel_a;
1501
1502 let vn = relative_vel.dot(&contact.normal);
1503 let tangent_vel = relative_vel - contact.normal * vn;
1504 let tangent_speed = tangent_vel.norm();
1505
1506 if tangent_speed > 1e-6 {
1507 let tangent = tangent_vel / tangent_speed;
1508
1509 let ra_cross_t = ra.cross(&tangent);
1511 let rb_cross_t = rb.cross(&tangent);
1512 let ang_t_a = (inv_inertia_a * ra_cross_t).cross(&ra).dot(&tangent);
1513 let ang_t_b = (inv_inertia_b * rb_cross_t).cross(&rb).dot(&tangent);
1514 let eff_mass_t_inv = inv_mass_a + inv_mass_b + ang_t_a + ang_t_b;
1515
1516 if eff_mass_t_inv > 0.0 {
1517 let jt = (-tangent_speed / eff_mass_t_inv).max(-mu * j);
1519
1520 let friction_impulse = tangent * jt;
1521 self.bodies[a].velocity -= friction_impulse * inv_mass_a;
1522 self.bodies[b].velocity += friction_impulse * inv_mass_b;
1523 self.bodies[a].angular_velocity -=
1524 inv_inertia_a * ra.cross(&friction_impulse);
1525 self.bodies[b].angular_velocity +=
1526 inv_inertia_b * rb.cross(&friction_impulse);
1527 }
1528 }
1529 }
1530 }
1531 }
1532
1533 pub fn solve_constraints(&mut self, dt: f32) {
1541 if self.constraints.is_empty() || dt <= 0.0 {
1542 return;
1543 }
1544
1545 let iterations = self.solver_iterations;
1546 for _ in 0..iterations {
1547 for ci in 0..self.constraints.len() {
1549 let constraint = self.constraints[ci].clone();
1551 match &constraint {
1552 Constraint::Distance {
1553 body_a,
1554 body_b,
1555 anchor_a,
1556 anchor_b,
1557 rest_length,
1558 compliance,
1559 } => {
1560 self.solve_distance(
1561 body_a.0,
1562 anchor_a,
1563 body_b.0,
1564 anchor_b,
1565 *rest_length,
1566 *compliance,
1567 dt,
1568 );
1569 }
1570 Constraint::BallSocket {
1571 body_a,
1572 body_b,
1573 anchor_a,
1574 anchor_b,
1575 compliance,
1576 } => {
1577 self.solve_distance(
1578 body_a.0,
1579 anchor_a,
1580 body_b.0,
1581 anchor_b,
1582 0.0,
1583 *compliance,
1584 dt,
1585 );
1586 }
1587 Constraint::Fixed {
1588 body_a,
1589 body_b,
1590 anchor_a,
1591 anchor_b,
1592 relative_orientation,
1593 compliance,
1594 } => {
1595 self.solve_distance(
1597 body_a.0,
1598 anchor_a,
1599 body_b.0,
1600 anchor_b,
1601 0.0,
1602 *compliance,
1603 dt,
1604 );
1605 self.solve_orientation(
1607 body_a.0,
1608 body_b.0,
1609 relative_orientation,
1610 *compliance,
1611 dt,
1612 );
1613 }
1614 }
1615 }
1616 }
1617 }
1618
1619 fn solve_distance(
1624 &mut self,
1625 a: usize,
1626 anchor_a: &Vector3<f32>,
1627 b: usize,
1628 anchor_b: &Vector3<f32>,
1629 rest_length: f32,
1630 compliance: f32,
1631 dt: f32,
1632 ) {
1633 if !self.bodies[a].active && !self.bodies[b].active {
1634 return;
1635 }
1636
1637 let ra = self.bodies[a].orientation * anchor_a;
1639 let rb = self.bodies[b].orientation * anchor_b;
1640 let world_a = self.bodies[a].position + ra;
1641 let world_b = self.bodies[b].position + rb;
1642
1643 let diff = world_b - world_a;
1644 let dist = diff.norm();
1645
1646 let error = dist - rest_length;
1647 if error.abs() < 1e-6 {
1648 return;
1649 }
1650
1651 let n = if dist > 1e-6 {
1653 diff / dist
1654 } else {
1655 Vector3::new(0.0, 1.0, 0.0) };
1657
1658 let inv_mass_a = self.bodies[a].inv_mass;
1660 let inv_mass_b = self.bodies[b].inv_mass;
1661 let inv_inertia_a = self.bodies[a].inv_inertia_world();
1662 let inv_inertia_b = self.bodies[b].inv_inertia_world();
1663
1664 let ra_cross_n = ra.cross(&n);
1665 let rb_cross_n = rb.cross(&n);
1666 let w_a = inv_mass_a + ra_cross_n.dot(&(inv_inertia_a * ra_cross_n));
1667 let w_b = inv_mass_b + rb_cross_n.dot(&(inv_inertia_b * rb_cross_n));
1668 let w_sum = w_a + w_b;
1669
1670 if w_sum <= 0.0 {
1671 return;
1672 }
1673
1674 let alpha = compliance / (dt * dt);
1676 let delta_lambda = -error / (w_sum + alpha);
1677 let correction = n * delta_lambda;
1678
1679 self.bodies[a].position -= correction * inv_mass_a;
1681 self.bodies[b].position += correction * inv_mass_b;
1682
1683 self.bodies[a].angular_velocity -= inv_inertia_a * ra.cross(&correction) / dt;
1685 self.bodies[b].angular_velocity += inv_inertia_b * rb.cross(&correction) / dt;
1686
1687 self.bodies[a].velocity -= correction * inv_mass_a / dt;
1689 self.bodies[b].velocity += correction * inv_mass_b / dt;
1690 }
1691
1692 fn solve_orientation(
1696 &mut self,
1697 a: usize,
1698 b: usize,
1699 target_rel: &UnitQuaternion<f32>,
1700 compliance: f32,
1701 dt: f32,
1702 ) {
1703 if !self.bodies[a].active && !self.bodies[b].active {
1704 return;
1705 }
1706
1707 let qa = self.bodies[a].orientation;
1708 let qb = self.bodies[b].orientation;
1709
1710 let current_rel = qb * qa.inverse();
1712 let error_q = current_rel * target_rel.inverse();
1713
1714 let error_inner = error_q.into_inner();
1716 let error_vec = Vector3::new(error_inner.i, error_inner.j, error_inner.k);
1717
1718 let error = if error_inner.w >= 0.0 {
1720 error_vec * 2.0
1721 } else {
1722 -error_vec * 2.0
1723 };
1724
1725 if error.norm_squared() < 1e-10 {
1726 return;
1727 }
1728
1729 let inv_inertia_a = self.bodies[a].inv_inertia_world();
1730 let inv_inertia_b = self.bodies[b].inv_inertia_world();
1731
1732 let w_a = if self.bodies[a].body_type == BodyType::Dynamic {
1736 inv_inertia_a[(0, 0)] + inv_inertia_a[(1, 1)] + inv_inertia_a[(2, 2)]
1737 } else {
1738 0.0
1739 };
1740 let w_b = if self.bodies[b].body_type == BodyType::Dynamic {
1741 inv_inertia_b[(0, 0)] + inv_inertia_b[(1, 1)] + inv_inertia_b[(2, 2)]
1742 } else {
1743 0.0
1744 };
1745 let w_sum = w_a + w_b;
1746 if w_sum <= 0.0 {
1747 return;
1748 }
1749
1750 let alpha = compliance / (dt * dt);
1751 let delta_lambda = -error / (w_sum + alpha);
1752
1753 if self.bodies[a].body_type == BodyType::Dynamic {
1755 self.bodies[a].angular_velocity -= inv_inertia_a * delta_lambda / dt;
1756 }
1757 if self.bodies[b].body_type == BodyType::Dynamic {
1758 self.bodies[b].angular_velocity += inv_inertia_b * delta_lambda / dt;
1759 }
1760 }
1761
1762 pub fn step<const C: usize>(&mut self, dt: f32) {
1769 let gravity = self.gravity;
1770 for body in self.bodies.iter_mut() {
1771 if body.active {
1772 body.integrate(dt, gravity);
1773 }
1774 }
1775
1776 let contacts = self.detect_collisions::<C>();
1777 self.resolve_contacts(&contacts);
1778 self.solve_constraints(dt);
1779 }
1780
1781 pub fn step_fixed<const C: usize>(&mut self, dt: f32, substeps: u32) {
1786 let sub_dt = dt / substeps as f32;
1787 for _ in 0..substeps {
1788 self.step::<C>(sub_dt);
1789 }
1790 }
1791}
1792
1793pub fn sync_body_to_mesh(body: &RigidBody, mesh: &mut crate::mesh::K3dMesh<'_>) {
1806 mesh.set_position(body.position.x, body.position.y, body.position.z);
1807 mesh.set_rotation(body.orientation);
1808}
1809
1810#[cfg(test)]
1811mod tests {
1812 extern crate std;
1813 use super::*;
1814
1815 const EPSILON: f32 = 1e-4;
1816
1817 fn approx_eq(a: f32, b: f32) -> bool {
1818 (a - b).abs() < EPSILON
1819 }
1820
1821 fn approx_vec_eq(a: &Vector3<f32>, b: &Vector3<f32>) -> bool {
1822 approx_eq(a.x, b.x) && approx_eq(a.y, b.y) && approx_eq(a.z, b.z)
1823 }
1824
1825 #[test]
1828 fn test_body_creation() {
1829 let body = RigidBody::new(5.0);
1830 assert_eq!(body.mass, 5.0);
1831 assert!(approx_eq(body.inv_mass, 0.2));
1832 assert_eq!(body.body_type, BodyType::Dynamic);
1833 assert!(approx_vec_eq(&body.position, &Vector3::zeros()));
1834 assert!(approx_vec_eq(&body.velocity, &Vector3::zeros()));
1835 }
1836
1837 #[test]
1838 fn test_static_body() {
1839 let body = RigidBody::new_static();
1840 assert_eq!(body.body_type, BodyType::Static);
1841 assert_eq!(body.inv_mass, 0.0);
1842 assert!(body.mass.is_infinite());
1843 }
1844
1845 #[test]
1846 #[should_panic]
1847 fn test_body_zero_mass_panics() {
1848 RigidBody::new(0.0);
1849 }
1850
1851 #[test]
1852 #[should_panic]
1853 fn test_body_negative_mass_panics() {
1854 RigidBody::new(-1.0);
1855 }
1856
1857 #[test]
1858 fn test_builder_pattern() {
1859 let body = RigidBody::new(1.0)
1860 .with_position(Vector3::new(1.0, 2.0, 3.0))
1861 .with_velocity(Vector3::new(0.0, 5.0, 0.0))
1862 .with_restitution(0.8)
1863 .with_damping(0.05);
1864
1865 assert!(approx_vec_eq(&body.position, &Vector3::new(1.0, 2.0, 3.0)));
1866 assert!(approx_vec_eq(&body.velocity, &Vector3::new(0.0, 5.0, 0.0)));
1867 assert!(approx_eq(body.restitution, 0.8));
1868 assert!(approx_eq(body.damping, 0.05));
1869 }
1870
1871 #[test]
1872 fn test_apply_force() {
1873 let mut body = RigidBody::new(1.0);
1874 body.apply_force(Vector3::new(10.0, 0.0, 0.0));
1875 body.apply_force(Vector3::new(0.0, 5.0, 0.0));
1876
1877 assert!(approx_vec_eq(
1879 &body.force_accumulator,
1880 &Vector3::new(10.0, 5.0, 0.0)
1881 ));
1882 }
1883
1884 #[test]
1885 fn test_apply_impulse() {
1886 let mut body = RigidBody::new(2.0);
1887 body.apply_impulse(Vector3::new(10.0, 0.0, 0.0));
1888
1889 assert!(approx_vec_eq(&body.velocity, &Vector3::new(5.0, 0.0, 0.0)));
1891 }
1892
1893 #[test]
1894 fn test_impulse_on_static_body_ignored() {
1895 let mut body = RigidBody::new_static();
1896 body.apply_impulse(Vector3::new(100.0, 0.0, 0.0));
1897 assert!(approx_vec_eq(&body.velocity, &Vector3::zeros()));
1898 }
1899
1900 #[test]
1901 fn test_speed() {
1902 let body = RigidBody::new(1.0).with_velocity(Vector3::new(3.0, 4.0, 0.0));
1903 assert!(approx_eq(body.speed(), 5.0));
1904 }
1905
1906 #[test]
1907 fn test_kinetic_energy() {
1908 let body = RigidBody::new(2.0).with_velocity(Vector3::new(3.0, 0.0, 0.0));
1909 assert!(approx_eq(body.kinetic_energy(), 9.0));
1911 }
1912
1913 #[test]
1916 fn test_world_creation() {
1917 let world = PhysicsWorld::<8>::new();
1918 assert_eq!(world.body_count(), 0);
1919 assert!(approx_vec_eq(&world.gravity(), &Vector3::zeros()));
1920 }
1921
1922 #[test]
1923 fn test_add_and_get_body() {
1924 let mut world = PhysicsWorld::<8>::new();
1925 let body = RigidBody::new(1.0).with_position(Vector3::new(1.0, 2.0, 3.0));
1926 let id = world.add_body(body).unwrap();
1927
1928 assert_eq!(world.body_count(), 1);
1929 let b = world.body(id).unwrap();
1930 assert!(approx_vec_eq(&b.position, &Vector3::new(1.0, 2.0, 3.0)));
1931 }
1932
1933 #[test]
1934 fn test_add_body_at_capacity() {
1935 let mut world = PhysicsWorld::<2>::new();
1936 assert!(world.add_body(RigidBody::new(1.0)).is_some());
1937 assert!(world.add_body(RigidBody::new(1.0)).is_some());
1938 assert!(world.add_body(RigidBody::new(1.0)).is_none()); }
1940
1941 #[test]
1942 fn test_gravity_freefall() {
1943 let mut world = PhysicsWorld::<4>::new();
1944 world.set_gravity(Vector3::new(0.0, -10.0, 0.0));
1945
1946 let body = RigidBody::new(1.0)
1947 .with_position(Vector3::new(0.0, 100.0, 0.0))
1948 .with_damping(0.0);
1949 let id = world.add_body(body).unwrap();
1950
1951 world.step::<4>(1.0);
1953
1954 let b = world.body(id).unwrap();
1955 assert!(approx_eq(b.velocity.y, -10.0));
1959 assert!(approx_eq(b.position.y, 90.0));
1960 }
1961
1962 #[test]
1963 fn test_static_body_not_affected_by_gravity() {
1964 let mut world = PhysicsWorld::<4>::new();
1965 world.set_gravity(Vector3::new(0.0, -9.81, 0.0));
1966
1967 let body = RigidBody::new_static().with_position(Vector3::new(0.0, 0.0, 0.0));
1968 let id = world.add_body(body).unwrap();
1969
1970 world.step::<4>(1.0);
1971
1972 let b = world.body(id).unwrap();
1973 assert!(approx_vec_eq(&b.position, &Vector3::zeros()));
1974 assert!(approx_vec_eq(&b.velocity, &Vector3::zeros()));
1975 }
1976
1977 #[test]
1978 fn test_force_accumulation_and_clearing() {
1979 let mut world = PhysicsWorld::<4>::new();
1980
1981 let body = RigidBody::new(1.0).with_damping(0.0);
1982 let id = world.add_body(body).unwrap();
1983
1984 world
1986 .body_mut(id)
1987 .unwrap()
1988 .apply_force(Vector3::new(10.0, 0.0, 0.0));
1989 world.step::<4>(1.0);
1990
1991 let b = world.body(id).unwrap();
1992 assert!(approx_eq(b.velocity.x, 10.0));
1993
1994 world.step::<4>(1.0);
1996 let b = world.body(id).unwrap();
1997 assert!(approx_eq(b.velocity.x, 10.0)); }
1999
2000 #[test]
2001 fn test_damping_reduces_velocity() {
2002 let mut world = PhysicsWorld::<4>::new();
2003
2004 let body = RigidBody::new(1.0)
2005 .with_velocity(Vector3::new(10.0, 0.0, 0.0))
2006 .with_damping(0.1);
2007 let id = world.add_body(body).unwrap();
2008
2009 world.step::<4>(1.0);
2010
2011 let b = world.body(id).unwrap();
2012 assert!(b.velocity.x < 10.0);
2014 assert!(b.velocity.x > 0.0);
2015 }
2016
2017 #[test]
2018 fn test_step_fixed_substeps() {
2019 let mut world_single = PhysicsWorld::<4>::new();
2020 let mut world_sub = PhysicsWorld::<4>::new();
2021
2022 world_single.set_gravity(Vector3::new(0.0, -10.0, 0.0));
2023 world_sub.set_gravity(Vector3::new(0.0, -10.0, 0.0));
2024
2025 let body = RigidBody::new(1.0)
2026 .with_position(Vector3::new(0.0, 100.0, 0.0))
2027 .with_damping(0.0);
2028
2029 let id1 = world_single.add_body(body.clone()).unwrap();
2030 let id2 = world_sub.add_body(body).unwrap();
2031
2032 world_single.step::<4>(1.0);
2034 world_sub.step_fixed::<4>(1.0, 10);
2035
2036 let b1 = world_single.body(id1).unwrap();
2038 let b2 = world_sub.body(id2).unwrap();
2039
2040 assert!(approx_eq(b1.velocity.y, b2.velocity.y));
2042 }
2043
2044 #[test]
2045 fn test_bodies_iterator() {
2046 let mut world = PhysicsWorld::<4>::new();
2047 world
2048 .add_body(RigidBody::new(1.0).with_position(Vector3::new(1.0, 0.0, 0.0)))
2049 .unwrap();
2050 world
2051 .add_body(RigidBody::new(2.0).with_position(Vector3::new(2.0, 0.0, 0.0)))
2052 .unwrap();
2053
2054 let positions: std::vec::Vec<f32> = world.bodies().map(|(_, b)| b.position.x).collect();
2055 assert_eq!(positions, std::vec![1.0, 2.0]);
2056 }
2057
2058 #[test]
2059 fn test_projectile_motion() {
2060 let mut world = PhysicsWorld::<4>::new();
2061 world.set_gravity(Vector3::new(0.0, -10.0, 0.0));
2062
2063 let body = RigidBody::new(1.0)
2065 .with_velocity(Vector3::new(10.0, 10.0, 0.0))
2066 .with_damping(0.0);
2067 let id = world.add_body(body).unwrap();
2068
2069 for _ in 0..200 {
2071 world.step::<4>(0.01);
2072 }
2073
2074 let b = world.body(id).unwrap();
2075 assert!((b.position.x - 20.0).abs() < 0.5);
2077 assert!((b.velocity.y - (-10.0)).abs() < 0.5);
2079 }
2080
2081 #[test]
2084 fn test_sphere_sphere_no_collision() {
2085 let result = collide_sphere_sphere(
2086 &Vector3::new(0.0, 0.0, 0.0),
2087 1.0,
2088 &Vector3::new(3.0, 0.0, 0.0),
2089 1.0,
2090 );
2091 assert!(result.is_none());
2092 }
2093
2094 #[test]
2095 fn test_sphere_sphere_touching() {
2096 let result = collide_sphere_sphere(
2097 &Vector3::new(0.0, 0.0, 0.0),
2098 1.0,
2099 &Vector3::new(2.0, 0.0, 0.0),
2100 1.0,
2101 );
2102 assert!(result.is_none());
2104 }
2105
2106 #[test]
2107 fn test_sphere_sphere_overlapping() {
2108 let result = collide_sphere_sphere(
2109 &Vector3::new(0.0, 0.0, 0.0),
2110 1.0,
2111 &Vector3::new(1.5, 0.0, 0.0),
2112 1.0,
2113 );
2114 let (normal, penetration) = result.unwrap();
2115 assert!(approx_eq(penetration, 0.5));
2116 assert!(normal.x > 0.0);
2118 assert!(approx_eq(normal.y, 0.0));
2119 }
2120
2121 #[test]
2122 fn test_sphere_sphere_coincident() {
2123 let result = collide_sphere_sphere(
2124 &Vector3::new(0.0, 0.0, 0.0),
2125 1.0,
2126 &Vector3::new(0.0, 0.0, 0.0),
2127 1.0,
2128 );
2129 let (normal, penetration) = result.unwrap();
2130 assert!(approx_eq(penetration, 2.0));
2131 assert!(approx_eq(normal.norm(), 1.0));
2133 }
2134
2135 #[test]
2136 fn test_aabb_aabb_no_collision() {
2137 let result = collide_aabb_aabb(
2138 &Vector3::new(0.0, 0.0, 0.0),
2139 &Vector3::new(1.0, 1.0, 1.0),
2140 &Vector3::new(3.0, 0.0, 0.0),
2141 &Vector3::new(1.0, 1.0, 1.0),
2142 );
2143 assert!(result.is_none());
2144 }
2145
2146 #[test]
2147 fn test_aabb_aabb_overlapping_x() {
2148 let result = collide_aabb_aabb(
2149 &Vector3::new(0.0, 0.0, 0.0),
2150 &Vector3::new(1.0, 1.0, 1.0),
2151 &Vector3::new(1.5, 0.0, 0.0),
2152 &Vector3::new(1.0, 1.0, 1.0),
2153 );
2154 let (normal, penetration) = result.unwrap();
2155 assert!(approx_eq(penetration, 0.5));
2157 assert!(approx_eq(normal.x, 1.0));
2158 }
2159
2160 #[test]
2161 fn test_aabb_aabb_overlapping_y() {
2162 let result = collide_aabb_aabb(
2163 &Vector3::new(0.0, 0.0, 0.0),
2164 &Vector3::new(1.0, 1.0, 1.0),
2165 &Vector3::new(0.0, 1.5, 0.0),
2166 &Vector3::new(1.0, 1.0, 1.0),
2167 );
2168 let (normal, penetration) = result.unwrap();
2169 assert!(approx_eq(penetration, 0.5));
2170 assert!(approx_eq(normal.y, 1.0));
2171 }
2172
2173 #[test]
2174 fn test_aabb_aabb_negative_direction() {
2175 let result = collide_aabb_aabb(
2176 &Vector3::new(0.0, 0.0, 0.0),
2177 &Vector3::new(1.0, 1.0, 1.0),
2178 &Vector3::new(-1.5, 0.0, 0.0),
2179 &Vector3::new(1.0, 1.0, 1.0),
2180 );
2181 let (normal, _) = result.unwrap();
2182 assert!(approx_eq(normal.x, -1.0));
2184 }
2185
2186 #[test]
2187 fn test_sphere_aabb_no_collision() {
2188 let result = collide_sphere_aabb(
2189 &Vector3::new(5.0, 0.0, 0.0),
2190 1.0,
2191 &Vector3::new(0.0, 0.0, 0.0),
2192 &Vector3::new(1.0, 1.0, 1.0),
2193 );
2194 assert!(result.is_none());
2195 }
2196
2197 #[test]
2198 fn test_sphere_aabb_overlapping() {
2199 let result = collide_sphere_aabb(
2203 &Vector3::new(1.5, 0.0, 0.0),
2204 1.0,
2205 &Vector3::new(0.0, 0.0, 0.0),
2206 &Vector3::new(1.0, 1.0, 1.0),
2207 );
2208 let (normal, penetration) = result.unwrap();
2209 assert!(approx_eq(penetration, 0.5));
2210 assert!(normal.x < 0.0);
2212 }
2213
2214 #[test]
2215 fn test_sphere_aabb_sphere_inside() {
2216 let result = collide_sphere_aabb(
2218 &Vector3::new(0.0, 0.0, 0.0),
2219 0.5,
2220 &Vector3::new(0.0, 0.0, 0.0),
2221 &Vector3::new(2.0, 1.0, 3.0),
2222 );
2223 let (normal, penetration) = result.unwrap();
2224 assert!(penetration > 0.0);
2226 assert!(approx_eq(normal.norm(), 1.0));
2227 }
2228
2229 #[test]
2230 fn test_sphere_aabb_from_below() {
2231 let result = collide_sphere_aabb(
2233 &Vector3::new(0.0, -1.5, 0.0),
2234 1.0,
2235 &Vector3::new(0.0, 0.0, 0.0),
2236 &Vector3::new(2.0, 1.0, 2.0),
2237 );
2238 let (normal, penetration) = result.unwrap();
2239 assert!(penetration > 0.0);
2240 assert!(normal.y > 0.0);
2242 }
2243
2244 #[test]
2247 fn test_detect_collisions_no_colliders() {
2248 let mut world = PhysicsWorld::<4>::new();
2249 world.add_body(RigidBody::new(1.0)).unwrap();
2250 world.add_body(RigidBody::new(1.0)).unwrap();
2251
2252 let contacts = world.detect_collisions::<4>();
2253 assert_eq!(contacts.len(), 0);
2254 }
2255
2256 #[test]
2257 fn test_detect_collisions_sphere_sphere() {
2258 let mut world = PhysicsWorld::<4>::new();
2259 world
2260 .add_body(
2261 RigidBody::new(1.0)
2262 .with_position(Vector3::new(0.0, 0.0, 0.0))
2263 .with_collider(Collider::Sphere { radius: 1.0 }),
2264 )
2265 .unwrap();
2266 world
2267 .add_body(
2268 RigidBody::new(1.0)
2269 .with_position(Vector3::new(1.5, 0.0, 0.0))
2270 .with_collider(Collider::Sphere { radius: 1.0 }),
2271 )
2272 .unwrap();
2273
2274 let contacts = world.detect_collisions::<4>();
2275 assert_eq!(contacts.len(), 1);
2276 assert!(approx_eq(contacts[0].penetration, 0.5));
2277 }
2278
2279 #[test]
2280 fn test_detect_collisions_skips_static_pairs() {
2281 let mut world = PhysicsWorld::<4>::new();
2282 world
2283 .add_body(
2284 RigidBody::new_static()
2285 .with_position(Vector3::new(0.0, 0.0, 0.0))
2286 .with_collider(Collider::Sphere { radius: 1.0 }),
2287 )
2288 .unwrap();
2289 world
2290 .add_body(
2291 RigidBody::new_static()
2292 .with_position(Vector3::new(0.5, 0.0, 0.0))
2293 .with_collider(Collider::Sphere { radius: 1.0 }),
2294 )
2295 .unwrap();
2296
2297 let contacts = world.detect_collisions::<4>();
2298 assert_eq!(contacts.len(), 0);
2299 }
2300
2301 #[test]
2302 fn test_collision_response_separates_bodies() {
2303 let mut world = PhysicsWorld::<4>::new();
2304 world
2305 .add_body(
2306 RigidBody::new(1.0)
2307 .with_position(Vector3::new(0.0, 0.0, 0.0))
2308 .with_collider(Collider::Sphere { radius: 1.0 })
2309 .with_restitution(0.0)
2310 .with_damping(0.0),
2311 )
2312 .unwrap();
2313 world
2314 .add_body(
2315 RigidBody::new(1.0)
2316 .with_position(Vector3::new(1.0, 0.0, 0.0))
2317 .with_collider(Collider::Sphere { radius: 1.0 })
2318 .with_restitution(0.0)
2319 .with_damping(0.0),
2320 )
2321 .unwrap();
2322
2323 let contacts = world.detect_collisions::<4>();
2324 assert_eq!(contacts.len(), 1);
2325
2326 world.resolve_contacts(&contacts);
2327
2328 let a = &world.body(BodyId(0)).unwrap().position;
2330 let b = &world.body(BodyId(1)).unwrap().position;
2331 let dist = (b - a).norm();
2332 assert!(dist >= 2.0 - EPSILON);
2333 }
2334
2335 #[test]
2336 fn test_collision_response_bounce() {
2337 let mut world = PhysicsWorld::<4>::new();
2338 world
2340 .add_body(
2341 RigidBody::new(1.0)
2342 .with_position(Vector3::new(0.0, 0.0, 0.0))
2343 .with_velocity(Vector3::new(5.0, 0.0, 0.0))
2344 .with_collider(Collider::Sphere { radius: 1.0 })
2345 .with_restitution(1.0)
2346 .with_damping(0.0),
2347 )
2348 .unwrap();
2349 world
2351 .add_body(
2352 RigidBody::new(1.0)
2353 .with_position(Vector3::new(1.5, 0.0, 0.0))
2354 .with_velocity(Vector3::zeros())
2355 .with_collider(Collider::Sphere { radius: 1.0 })
2356 .with_restitution(1.0)
2357 .with_damping(0.0),
2358 )
2359 .unwrap();
2360
2361 let contacts = world.detect_collisions::<4>();
2362 world.resolve_contacts(&contacts);
2363
2364 let vel_a = world.body(BodyId(0)).unwrap().velocity;
2365 let vel_b = world.body(BodyId(1)).unwrap().velocity;
2366
2367 assert!(approx_eq(vel_a.x, 0.0));
2370 assert!(approx_eq(vel_b.x, 5.0));
2371 }
2372
2373 #[test]
2374 fn test_collision_dynamic_vs_static() {
2375 let mut world = PhysicsWorld::<4>::new();
2376 world
2378 .add_body(
2379 RigidBody::new(1.0)
2380 .with_position(Vector3::new(0.0, 0.5, 0.0))
2381 .with_velocity(Vector3::new(0.0, -5.0, 0.0))
2382 .with_collider(Collider::Sphere { radius: 1.0 })
2383 .with_restitution(0.5)
2384 .with_damping(0.0),
2385 )
2386 .unwrap();
2387 world
2389 .add_body(
2390 RigidBody::new_static()
2391 .with_position(Vector3::new(0.0, -1.0, 0.0))
2392 .with_collider(Collider::Aabb {
2393 half_extents: Vector3::new(10.0, 1.0, 10.0),
2394 })
2395 .with_restitution(1.0),
2396 )
2397 .unwrap();
2398
2399 let contacts = world.detect_collisions::<4>();
2400 assert_eq!(contacts.len(), 1);
2401
2402 world.resolve_contacts(&contacts);
2403
2404 let ball = world.body(BodyId(0)).unwrap();
2406 assert!(ball.velocity.y > 0.0);
2407
2408 let floor = world.body(BodyId(1)).unwrap();
2410 assert!(approx_vec_eq(&floor.velocity, &Vector3::zeros()));
2411 assert!(approx_eq(floor.position.y, -1.0));
2412 }
2413
2414 #[test]
2415 fn test_step_with_collisions() {
2416 let mut world = PhysicsWorld::<4>::new();
2417 world.set_gravity(Vector3::new(0.0, -10.0, 0.0));
2418
2419 world
2421 .add_body(
2422 RigidBody::new(1.0)
2423 .with_position(Vector3::new(0.0, 2.0, 0.0))
2424 .with_collider(Collider::Sphere { radius: 0.5 })
2425 .with_restitution(0.5)
2426 .with_damping(0.0),
2427 )
2428 .unwrap();
2429 world
2430 .add_body(
2431 RigidBody::new_static()
2432 .with_position(Vector3::new(0.0, 0.0, 0.0))
2433 .with_collider(Collider::Aabb {
2434 half_extents: Vector3::new(10.0, 0.1, 10.0),
2435 }),
2436 )
2437 .unwrap();
2438
2439 for _ in 0..600 {
2441 world.step::<4>(1.0 / 60.0);
2442 }
2443
2444 let ball = world.body(BodyId(0)).unwrap();
2445 assert!(ball.position.y >= 0.5);
2447 }
2448
2449 #[test]
2450 fn test_collider_builder() {
2451 let body = RigidBody::new(1.0).with_collider(Collider::Sphere { radius: 2.0 });
2452 assert_eq!(body.collider, Some(Collider::Sphere { radius: 2.0 }));
2453
2454 let body2 = RigidBody::new(1.0).with_collider(Collider::Aabb {
2455 half_extents: Vector3::new(1.0, 2.0, 3.0),
2456 });
2457 assert!(matches!(body2.collider, Some(Collider::Aabb { .. })));
2458 }
2459
2460 #[test]
2461 fn test_no_collider_body_ignored() {
2462 let mut world = PhysicsWorld::<4>::new();
2463 world.add_body(RigidBody::new(1.0)).unwrap();
2465 world
2467 .add_body(RigidBody::new(1.0).with_collider(Collider::Sphere { radius: 100.0 }))
2468 .unwrap();
2469
2470 let contacts = world.detect_collisions::<4>();
2471 assert_eq!(contacts.len(), 0); }
2473
2474 #[test]
2475 fn test_aabb_sphere_collision_via_world() {
2476 let mut world = PhysicsWorld::<4>::new();
2477 world
2478 .add_body(
2479 RigidBody::new(1.0)
2480 .with_position(Vector3::new(0.0, 0.0, 0.0))
2481 .with_collider(Collider::Aabb {
2482 half_extents: Vector3::new(1.0, 1.0, 1.0),
2483 }),
2484 )
2485 .unwrap();
2486 world
2487 .add_body(
2488 RigidBody::new(1.0)
2489 .with_position(Vector3::new(1.5, 0.0, 0.0))
2490 .with_collider(Collider::Sphere { radius: 1.0 }),
2491 )
2492 .unwrap();
2493
2494 let contacts = world.detect_collisions::<4>();
2495 assert_eq!(contacts.len(), 1);
2496 assert!(contacts[0].penetration > 0.0);
2497 }
2498
2499 #[test]
2502 fn test_friction_builder() {
2503 let body = RigidBody::new(1.0).with_friction(0.7);
2504 assert!(approx_eq(body.friction, 0.7));
2505 }
2506
2507 #[test]
2508 fn test_friction_clamped() {
2509 let body = RigidBody::new(1.0).with_friction(2.0);
2510 assert!(approx_eq(body.friction, 1.0));
2511 let body2 = RigidBody::new(1.0).with_friction(-0.5);
2512 assert!(approx_eq(body2.friction, 0.0));
2513 }
2514
2515 #[test]
2516 fn test_default_friction_values() {
2517 let dynamic = RigidBody::new(1.0);
2518 assert!(approx_eq(dynamic.friction, 0.3));
2519 let static_body = RigidBody::new_static();
2520 assert!(approx_eq(static_body.friction, 0.5));
2521 }
2522
2523 #[test]
2524 fn test_friction_reduces_tangential_velocity() {
2525 let mut world = PhysicsWorld::<4>::new();
2527
2528 world
2530 .add_body(
2531 RigidBody::new(1.0)
2532 .with_position(Vector3::new(0.0, 0.55, 0.0))
2533 .with_velocity(Vector3::new(10.0, -0.1, 0.0))
2534 .with_collider(Collider::Sphere { radius: 0.5 })
2535 .with_restitution(0.0)
2536 .with_friction(0.8)
2537 .with_damping(0.0),
2538 )
2539 .unwrap();
2540 world
2542 .add_body(
2543 RigidBody::new_static()
2544 .with_position(Vector3::new(0.0, 0.0, 0.0))
2545 .with_collider(Collider::Aabb {
2546 half_extents: Vector3::new(50.0, 0.1, 50.0),
2547 })
2548 .with_friction(0.8),
2549 )
2550 .unwrap();
2551
2552 let initial_vx = 10.0_f32;
2553
2554 let contacts = world.detect_collisions::<4>();
2556 assert_eq!(contacts.len(), 1);
2557 world.resolve_contacts(&contacts);
2558
2559 let ball = world.body(BodyId(0)).unwrap();
2560 assert!(ball.velocity.x < initial_vx);
2562 assert!(ball.velocity.x >= 0.0); }
2564
2565 #[test]
2566 fn test_zero_friction_preserves_tangential_velocity() {
2567 let mut world = PhysicsWorld::<4>::new();
2568
2569 world
2571 .add_body(
2572 RigidBody::new(1.0)
2573 .with_position(Vector3::new(0.0, 0.55, 0.0))
2574 .with_velocity(Vector3::new(10.0, -1.0, 0.0))
2575 .with_collider(Collider::Sphere { radius: 0.5 })
2576 .with_restitution(0.0)
2577 .with_friction(0.0)
2578 .with_damping(0.0),
2579 )
2580 .unwrap();
2581 world
2582 .add_body(
2583 RigidBody::new_static()
2584 .with_position(Vector3::new(0.0, 0.0, 0.0))
2585 .with_collider(Collider::Aabb {
2586 half_extents: Vector3::new(50.0, 0.1, 50.0),
2587 })
2588 .with_friction(0.0),
2589 )
2590 .unwrap();
2591
2592 let contacts = world.detect_collisions::<4>();
2593 world.resolve_contacts(&contacts);
2594
2595 let ball = world.body(BodyId(0)).unwrap();
2596 assert!(approx_eq(ball.velocity.x, 10.0));
2598 }
2599
2600 #[test]
2601 fn test_high_friction_vs_low_friction() {
2602 let make_world = |friction: f32| -> PhysicsWorld<4> {
2604 let mut world = PhysicsWorld::<4>::new();
2605 world
2606 .add_body(
2607 RigidBody::new(1.0)
2608 .with_position(Vector3::new(0.0, 0.55, 0.0))
2609 .with_velocity(Vector3::new(10.0, -1.0, 0.0))
2610 .with_collider(Collider::Sphere { radius: 0.5 })
2611 .with_restitution(0.0)
2612 .with_friction(friction)
2613 .with_damping(0.0),
2614 )
2615 .unwrap();
2616 world
2617 .add_body(
2618 RigidBody::new_static()
2619 .with_position(Vector3::new(0.0, 0.0, 0.0))
2620 .with_collider(Collider::Aabb {
2621 half_extents: Vector3::new(50.0, 0.1, 50.0),
2622 })
2623 .with_friction(friction),
2624 )
2625 .unwrap();
2626 world
2627 };
2628
2629 let mut world_low = make_world(0.1);
2630 let mut world_high = make_world(1.0);
2631
2632 let contacts_low = world_low.detect_collisions::<4>();
2633 world_low.resolve_contacts(&contacts_low);
2634 let contacts_high = world_high.detect_collisions::<4>();
2635 world_high.resolve_contacts(&contacts_high);
2636
2637 let vx_low = world_low.body(BodyId(0)).unwrap().velocity.x;
2638 let vx_high = world_high.body(BodyId(0)).unwrap().velocity.x;
2639
2640 assert!(vx_high < vx_low);
2642 }
2643
2644 #[test]
2647 fn test_body_active_by_default() {
2648 let body = RigidBody::new(1.0);
2649 assert!(body.active);
2650 let static_body = RigidBody::new_static();
2651 assert!(static_body.active);
2652 }
2653
2654 #[test]
2655 fn test_remove_body() {
2656 let mut world = PhysicsWorld::<4>::new();
2657 let id = world
2658 .add_body(RigidBody::new(1.0).with_velocity(Vector3::new(5.0, 0.0, 0.0)))
2659 .unwrap();
2660
2661 assert_eq!(world.active_body_count(), 1);
2662 assert!(world.remove_body(id));
2663 assert_eq!(world.active_body_count(), 0);
2664 assert_eq!(world.body_count(), 1); let body = world.body(id).unwrap();
2668 assert!(!body.active);
2669 assert!(approx_vec_eq(&body.velocity, &Vector3::zeros()));
2670 }
2671
2672 #[test]
2673 fn test_remove_body_twice_returns_false() {
2674 let mut world = PhysicsWorld::<4>::new();
2675 let id = world.add_body(RigidBody::new(1.0)).unwrap();
2676
2677 assert!(world.remove_body(id));
2678 assert!(!world.remove_body(id)); }
2680
2681 #[test]
2682 fn test_remove_body_invalid_id() {
2683 let mut world = PhysicsWorld::<4>::new();
2684 assert!(!world.remove_body(BodyId(99)));
2685 }
2686
2687 #[test]
2688 fn test_inactive_body_not_integrated() {
2689 let mut world = PhysicsWorld::<4>::new();
2690 world.set_gravity(Vector3::new(0.0, -10.0, 0.0));
2691
2692 let id = world
2693 .add_body(
2694 RigidBody::new(1.0)
2695 .with_position(Vector3::new(0.0, 10.0, 0.0))
2696 .with_damping(0.0),
2697 )
2698 .unwrap();
2699
2700 world.remove_body(id);
2701 world.step::<4>(1.0);
2702
2703 let body = world.body(id).unwrap();
2705 assert!(approx_eq(body.position.y, 10.0));
2706 }
2707
2708 #[test]
2709 fn test_inactive_body_not_collided() {
2710 let mut world = PhysicsWorld::<4>::new();
2711
2712 let id_a = world
2714 .add_body(
2715 RigidBody::new(1.0)
2716 .with_position(Vector3::zeros())
2717 .with_collider(Collider::Sphere { radius: 1.0 }),
2718 )
2719 .unwrap();
2720 world
2721 .add_body(
2722 RigidBody::new(1.0)
2723 .with_position(Vector3::new(0.5, 0.0, 0.0))
2724 .with_collider(Collider::Sphere { radius: 1.0 }),
2725 )
2726 .unwrap();
2727
2728 let contacts = world.detect_collisions::<4>();
2730 assert_eq!(contacts.len(), 1);
2731
2732 world.remove_body(id_a);
2734 let contacts = world.detect_collisions::<4>();
2735 assert_eq!(contacts.len(), 0);
2736 }
2737
2738 #[test]
2739 fn test_set_active_reactivates_body() {
2740 let mut world = PhysicsWorld::<4>::new();
2741 world.set_gravity(Vector3::new(0.0, -10.0, 0.0));
2742
2743 let id = world
2744 .add_body(
2745 RigidBody::new(1.0)
2746 .with_position(Vector3::new(0.0, 10.0, 0.0))
2747 .with_damping(0.0),
2748 )
2749 .unwrap();
2750
2751 world.remove_body(id);
2753 world.step::<4>(1.0);
2754 assert!(approx_eq(world.body(id).unwrap().position.y, 10.0));
2755
2756 world.set_active(id, true);
2758 assert_eq!(world.active_body_count(), 1);
2759 world.step::<4>(1.0);
2760 assert!(world.body(id).unwrap().position.y < 10.0);
2761 }
2762
2763 #[test]
2764 fn test_active_body_count() {
2765 let mut world = PhysicsWorld::<8>::new();
2766 let id0 = world.add_body(RigidBody::new(1.0)).unwrap();
2767 let id1 = world.add_body(RigidBody::new(1.0)).unwrap();
2768 let _id2 = world.add_body(RigidBody::new(1.0)).unwrap();
2769
2770 assert_eq!(world.active_body_count(), 3);
2771
2772 world.remove_body(id0);
2773 assert_eq!(world.active_body_count(), 2);
2774
2775 world.remove_body(id1);
2776 assert_eq!(world.active_body_count(), 1);
2777 }
2778
2779 #[test]
2780 fn test_remove_preserves_other_body_ids() {
2781 let mut world = PhysicsWorld::<4>::new();
2782 world.set_gravity(Vector3::new(0.0, -10.0, 0.0));
2783
2784 let id0 = world
2785 .add_body(
2786 RigidBody::new(1.0)
2787 .with_position(Vector3::new(0.0, 10.0, 0.0))
2788 .with_damping(0.0),
2789 )
2790 .unwrap();
2791 let id1 = world
2792 .add_body(
2793 RigidBody::new(1.0)
2794 .with_position(Vector3::new(5.0, 10.0, 0.0))
2795 .with_damping(0.0),
2796 )
2797 .unwrap();
2798
2799 world.remove_body(id0);
2801 world.step::<4>(1.0);
2802
2803 assert!(approx_eq(world.body(id0).unwrap().position.y, 10.0));
2805 assert!(world.body(id1).unwrap().position.y < 10.0);
2806 }
2807
2808 #[test]
2811 fn test_default_orientation_is_identity() {
2812 let body = RigidBody::new(1.0);
2813 assert!(approx_eq(body.orientation.w, 1.0));
2814 assert!(approx_vec_eq(&body.angular_velocity, &Vector3::zeros()));
2815 }
2816
2817 #[test]
2818 fn test_angular_velocity_builder() {
2819 let body = RigidBody::new(1.0).with_angular_velocity(Vector3::new(0.0, 5.0, 0.0));
2820 assert!(approx_eq(body.angular_velocity.y, 5.0));
2821 }
2822
2823 #[test]
2824 fn test_angular_damping_builder() {
2825 let body = RigidBody::new(1.0).with_angular_damping(0.05);
2826 assert!(approx_eq(body.angular_damping, 0.05));
2827 }
2828
2829 #[test]
2830 fn test_angular_damping_clamped() {
2831 let body = RigidBody::new(1.0).with_angular_damping(2.0);
2832 assert!(approx_eq(body.angular_damping, 1.0));
2833 let body2 = RigidBody::new(1.0).with_angular_damping(-1.0);
2834 assert!(approx_eq(body2.angular_damping, 0.0));
2835 }
2836
2837 #[test]
2838 fn test_inertia_sphere() {
2839 let body = RigidBody::new(2.0).with_inertia_sphere(0.5);
2840 let inv_i = body.inv_inertia_local[(0, 0)];
2842 assert!(approx_eq(inv_i, 5.0));
2843 assert!(approx_eq(body.inv_inertia_local[(1, 1)], 5.0));
2845 assert!(approx_eq(body.inv_inertia_local[(2, 2)], 5.0));
2846 assert!(approx_eq(body.inv_inertia_local[(0, 1)], 0.0));
2847 }
2848
2849 #[test]
2850 fn test_inertia_box() {
2851 let body = RigidBody::new(12.0).with_inertia_box(Vector3::new(1.0, 2.0, 3.0));
2852 assert!(approx_eq(body.inv_inertia_local[(0, 0)], 1.0 / 52.0));
2857 assert!(approx_eq(body.inv_inertia_local[(1, 1)], 1.0 / 40.0));
2858 assert!(approx_eq(body.inv_inertia_local[(2, 2)], 1.0 / 20.0));
2859 }
2860
2861 #[test]
2862 fn test_inertia_static_body_unchanged() {
2863 let body = RigidBody::new_static().with_inertia_sphere(1.0);
2864 assert!(approx_eq(body.inv_inertia_local[(0, 0)], 0.0));
2866 }
2867
2868 #[test]
2869 fn test_apply_torque() {
2870 let mut body = RigidBody::new(1.0);
2871 body.apply_torque(Vector3::new(1.0, 0.0, 0.0));
2872 body.apply_torque(Vector3::new(0.0, 2.0, 0.0));
2873 assert!(approx_vec_eq(
2874 &body.torque_accumulator,
2875 &Vector3::new(1.0, 2.0, 0.0)
2876 ));
2877 }
2878
2879 #[test]
2880 fn test_apply_angular_impulse() {
2881 let mut body = RigidBody::new(1.0).with_inertia_sphere(1.0);
2882 body.apply_angular_impulse(Vector3::new(1.0, 0.0, 0.0));
2884 assert!(approx_eq(body.angular_velocity.x, 2.5));
2886 }
2887
2888 #[test]
2889 fn test_angular_impulse_on_static_body_ignored() {
2890 let mut body = RigidBody::new_static();
2891 body.apply_angular_impulse(Vector3::new(100.0, 0.0, 0.0));
2892 assert!(approx_vec_eq(&body.angular_velocity, &Vector3::zeros()));
2893 }
2894
2895 #[test]
2896 fn test_constant_angular_velocity_changes_orientation() {
2897 let mut world = PhysicsWorld::<4>::new();
2898 let id = world
2899 .add_body(
2900 RigidBody::new(1.0)
2901 .with_angular_velocity(Vector3::new(0.0, core::f32::consts::PI, 0.0))
2902 .with_angular_damping(0.0)
2903 .with_damping(0.0),
2904 )
2905 .unwrap();
2906
2907 for _ in 0..100 {
2909 world.step::<4>(0.01);
2910 }
2911
2912 let body = world.body(id).unwrap();
2913 assert!(body.orientation.w.abs() < 0.9); assert!(approx_eq(body.angular_velocity.y, core::f32::consts::PI));
2917 }
2918
2919 #[test]
2920 fn test_torque_produces_angular_acceleration() {
2921 let mut world = PhysicsWorld::<4>::new();
2922 let id = world
2923 .add_body(
2924 RigidBody::new(1.0)
2925 .with_inertia_sphere(1.0)
2926 .with_angular_damping(0.0)
2927 .with_damping(0.0),
2928 )
2929 .unwrap();
2930
2931 world
2933 .body_mut(id)
2934 .unwrap()
2935 .apply_torque(Vector3::new(0.0, 0.0, 1.0));
2936 world.step::<4>(1.0);
2937
2938 let body = world.body(id).unwrap();
2939 assert!(approx_eq(body.angular_velocity.z, 2.5));
2942 }
2943
2944 #[test]
2945 fn test_torque_cleared_after_step() {
2946 let mut world = PhysicsWorld::<4>::new();
2947 let id = world
2948 .add_body(
2949 RigidBody::new(1.0)
2950 .with_inertia_sphere(1.0)
2951 .with_angular_damping(0.0)
2952 .with_damping(0.0),
2953 )
2954 .unwrap();
2955
2956 world
2957 .body_mut(id)
2958 .unwrap()
2959 .apply_torque(Vector3::new(0.0, 0.0, 1.0));
2960 world.step::<4>(1.0);
2961
2962 let omega_before = world.body(id).unwrap().angular_velocity.z;
2964 world.step::<4>(1.0);
2965 let omega_after = world.body(id).unwrap().angular_velocity.z;
2966 assert!(approx_eq(omega_before, omega_after));
2967 }
2968
2969 #[test]
2970 fn test_angular_damping_reduces_spin() {
2971 let mut world = PhysicsWorld::<4>::new();
2972 let id = world
2973 .add_body(
2974 RigidBody::new(1.0)
2975 .with_angular_velocity(Vector3::new(10.0, 0.0, 0.0))
2976 .with_angular_damping(0.1)
2977 .with_damping(0.0),
2978 )
2979 .unwrap();
2980
2981 world.step::<4>(1.0);
2982
2983 let body = world.body(id).unwrap();
2984 assert!(body.angular_velocity.x < 10.0);
2985 assert!(body.angular_velocity.x > 0.0);
2986 }
2987
2988 #[test]
2989 fn test_orientation_stays_normalized() {
2990 let mut world = PhysicsWorld::<4>::new();
2991 let id = world
2992 .add_body(
2993 RigidBody::new(1.0)
2994 .with_angular_velocity(Vector3::new(3.0, 5.0, 7.0))
2995 .with_angular_damping(0.0)
2996 .with_damping(0.0),
2997 )
2998 .unwrap();
2999
3000 for _ in 0..1000 {
3002 world.step::<4>(0.01);
3003 }
3004
3005 let body = world.body(id).unwrap();
3006 let q = body.orientation.into_inner();
3007 let norm = (q.w * q.w + q.i * q.i + q.j * q.j + q.k * q.k).sqrt();
3008 assert!((norm - 1.0).abs() < 1e-3);
3009 }
3010
3011 #[test]
3012 fn test_collision_imparts_angular_velocity() {
3013 let mut world = PhysicsWorld::<4>::new();
3015
3016 world
3018 .add_body(
3019 RigidBody::new(1.0)
3020 .with_position(Vector3::new(0.0, 0.55, 0.0))
3021 .with_velocity(Vector3::new(5.0, -1.0, 0.0))
3022 .with_collider(Collider::Sphere { radius: 0.5 })
3023 .with_inertia_sphere(0.5)
3024 .with_restitution(0.0)
3025 .with_friction(0.8)
3026 .with_damping(0.0)
3027 .with_angular_damping(0.0),
3028 )
3029 .unwrap();
3030
3031 world
3033 .add_body(
3034 RigidBody::new_static()
3035 .with_position(Vector3::new(0.0, 0.0, 0.0))
3036 .with_collider(Collider::Aabb {
3037 half_extents: Vector3::new(50.0, 0.1, 50.0),
3038 })
3039 .with_friction(0.8),
3040 )
3041 .unwrap();
3042
3043 let contacts = world.detect_collisions::<4>();
3044 assert_eq!(contacts.len(), 1);
3045 world.resolve_contacts(&contacts);
3046
3047 let ball = world.body(BodyId(0)).unwrap();
3048 let omega_magnitude = ball.angular_velocity.norm();
3050 assert!(
3051 omega_magnitude > 0.01,
3052 "Expected angular velocity from friction, got {}",
3053 omega_magnitude
3054 );
3055 }
3056
3057 #[test]
3058 fn test_remove_body_clears_angular_state() {
3059 let mut world = PhysicsWorld::<4>::new();
3060 let id = world
3061 .add_body(RigidBody::new(1.0).with_angular_velocity(Vector3::new(5.0, 5.0, 5.0)))
3062 .unwrap();
3063
3064 world.remove_body(id);
3065 let body = world.body(id).unwrap();
3066 assert!(approx_vec_eq(&body.angular_velocity, &Vector3::zeros()));
3067 }
3068
3069 #[test]
3070 fn test_inv_inertia_world_rotated() {
3071 let mut body = RigidBody::new(1.0).with_inertia_box(Vector3::new(1.0, 2.0, 3.0));
3074
3075 let inv_i_local_00 = body.inv_inertia_local[(0, 0)];
3076 let inv_i_world_00 = body.inv_inertia_world()[(0, 0)];
3077 assert!(approx_eq(inv_i_local_00, inv_i_world_00));
3079
3080 body.orientation = UnitQuaternion::from_axis_angle(
3082 &nalgebra::Unit::new_normalize(Vector3::new(0.0, 1.0, 0.0)),
3083 core::f32::consts::FRAC_PI_2,
3084 );
3085 let inv_i_rotated = body.inv_inertia_world();
3086 assert!(approx_eq(
3088 inv_i_rotated[(0, 0)],
3089 body.inv_inertia_local[(2, 2)]
3090 ));
3091 assert!(approx_eq(
3092 inv_i_rotated[(2, 2)],
3093 body.inv_inertia_local[(0, 0)]
3094 ));
3095 }
3096
3097 #[test]
3098 fn test_elastic_collision_angular_conservation() {
3099 let mut world = PhysicsWorld::<4>::new();
3102 world
3103 .add_body(
3104 RigidBody::new(1.0)
3105 .with_position(Vector3::new(0.0, 0.0, 0.0))
3106 .with_velocity(Vector3::new(5.0, 0.0, 0.0))
3107 .with_collider(Collider::Sphere { radius: 1.0 })
3108 .with_inertia_sphere(1.0)
3109 .with_restitution(1.0)
3110 .with_friction(0.0)
3111 .with_damping(0.0)
3112 .with_angular_damping(0.0),
3113 )
3114 .unwrap();
3115 world
3116 .add_body(
3117 RigidBody::new(1.0)
3118 .with_position(Vector3::new(1.5, 0.0, 0.0))
3119 .with_velocity(Vector3::zeros())
3120 .with_collider(Collider::Sphere { radius: 1.0 })
3121 .with_inertia_sphere(1.0)
3122 .with_restitution(1.0)
3123 .with_friction(0.0)
3124 .with_damping(0.0)
3125 .with_angular_damping(0.0),
3126 )
3127 .unwrap();
3128
3129 let contacts = world.detect_collisions::<4>();
3130 world.resolve_contacts(&contacts);
3131
3132 let omega_a = world.body(BodyId(0)).unwrap().angular_velocity;
3133 let omega_b = world.body(BodyId(1)).unwrap().angular_velocity;
3134 assert!(omega_a.norm() < EPSILON);
3136 assert!(omega_b.norm() < EPSILON);
3137 }
3138
3139 #[test]
3142 fn test_add_constraint() {
3143 let mut world = PhysicsWorld::<4, 4>::new();
3144 let a = world.add_body(RigidBody::new(1.0)).unwrap();
3145 let b = world.add_body(RigidBody::new(1.0)).unwrap();
3146
3147 let cid = world
3148 .add_constraint(Constraint::Distance {
3149 body_a: a,
3150 body_b: b,
3151 anchor_a: Vector3::zeros(),
3152 anchor_b: Vector3::zeros(),
3153 rest_length: 2.0,
3154 compliance: 0.0,
3155 })
3156 .unwrap();
3157
3158 assert_eq!(world.constraint_count(), 1);
3159 assert!(world.constraint(cid).is_some());
3160 }
3161
3162 #[test]
3163 fn test_add_constraint_at_capacity() {
3164 let mut world = PhysicsWorld::<4, 1>::new();
3165 let a = world.add_body(RigidBody::new(1.0)).unwrap();
3166 let b = world.add_body(RigidBody::new(1.0)).unwrap();
3167
3168 let c = Constraint::BallSocket {
3169 body_a: a,
3170 body_b: b,
3171 anchor_a: Vector3::zeros(),
3172 anchor_b: Vector3::zeros(),
3173 compliance: 0.0,
3174 };
3175 assert!(world.add_constraint(c.clone()).is_some());
3176 assert!(world.add_constraint(c).is_none()); }
3178
3179 #[test]
3180 fn test_remove_constraint() {
3181 let mut world = PhysicsWorld::<4, 4>::new();
3182 let a = world.add_body(RigidBody::new(1.0)).unwrap();
3183 let b = world.add_body(RigidBody::new(1.0)).unwrap();
3184
3185 let cid = world
3186 .add_ball_socket(a, Vector3::zeros(), b, Vector3::zeros(), 0.0)
3187 .unwrap();
3188 assert_eq!(world.constraint_count(), 1);
3189
3190 assert!(world.remove_constraint(cid));
3191 assert_eq!(world.constraint_count(), 0);
3192 }
3193
3194 #[test]
3195 fn test_remove_constraint_invalid_id() {
3196 let mut world = PhysicsWorld::<4, 4>::new();
3197 assert!(!world.remove_constraint(ConstraintId(99)));
3198 }
3199
3200 #[test]
3201 fn test_distance_constraint_maintains_length() {
3202 let mut world = PhysicsWorld::<4, 4>::new();
3203 world.set_gravity(Vector3::new(0.0, -10.0, 0.0));
3204
3205 let a = world
3207 .add_body(
3208 RigidBody::new(1.0)
3209 .with_position(Vector3::new(0.0, 5.0, 0.0))
3210 .with_damping(0.0)
3211 .with_angular_damping(0.0),
3212 )
3213 .unwrap();
3214 let b = world
3215 .add_body(
3216 RigidBody::new(1.0)
3217 .with_position(Vector3::new(3.0, 5.0, 0.0))
3218 .with_damping(0.0)
3219 .with_angular_damping(0.0),
3220 )
3221 .unwrap();
3222
3223 world
3224 .add_distance_constraint(a, Vector3::zeros(), b, Vector3::zeros(), 0.0)
3225 .unwrap();
3226
3227 for _ in 0..200 {
3229 world.step::<4>(1.0 / 60.0);
3230 }
3231
3232 let pa = world.body(a).unwrap().position;
3233 let pb = world.body(b).unwrap().position;
3234 let dist = (pb - pa).norm();
3235 assert!(
3237 (dist - 3.0).abs() < 0.3,
3238 "Expected distance ~3.0, got {}",
3239 dist
3240 );
3241 }
3242
3243 #[test]
3244 fn test_ball_socket_keeps_points_together() {
3245 let mut world = PhysicsWorld::<4, 4>::new();
3248 world.set_gravity(Vector3::new(0.0, -10.0, 0.0));
3249 world.solver_iterations = 8;
3250
3251 let a = world
3252 .add_body(RigidBody::new_static().with_position(Vector3::new(0.0, 10.0, 0.0)))
3253 .unwrap();
3254
3255 let b = world
3256 .add_body(
3257 RigidBody::new(1.0)
3258 .with_position(Vector3::new(0.0, 10.0, 0.0))
3259 .with_damping(0.01)
3260 .with_angular_damping(0.01),
3261 )
3262 .unwrap();
3263
3264 world
3266 .add_ball_socket(a, Vector3::zeros(), b, Vector3::zeros(), 0.0)
3267 .unwrap();
3268
3269 for _ in 0..120 {
3270 world.step::<4>(1.0 / 60.0);
3271 }
3272
3273 let pa = world.body(a).unwrap().position;
3275 let pb = world.body(b).unwrap().position;
3276 let gap = (pb - pa).norm();
3277 assert!(gap < 1.0, "Ball-socket gap too large: {}", gap);
3278 }
3279
3280 #[test]
3281 fn test_pendulum_swings_under_gravity() {
3282 let mut world = PhysicsWorld::<4, 4>::new();
3283 world.set_gravity(Vector3::new(0.0, -10.0, 0.0));
3284
3285 let anchor = world
3286 .add_body(RigidBody::new_static().with_position(Vector3::new(0.0, 10.0, 0.0)))
3287 .unwrap();
3288
3289 let bob = world
3290 .add_body(
3291 RigidBody::new(1.0)
3292 .with_position(Vector3::new(3.0, 10.0, 0.0))
3293 .with_damping(0.0)
3294 .with_angular_damping(0.0),
3295 )
3296 .unwrap();
3297
3298 world
3300 .add_distance_constraint(anchor, Vector3::zeros(), bob, Vector3::zeros(), 0.0)
3301 .unwrap();
3302
3303 let initial_y = 10.0;
3305
3306 for _ in 0..120 {
3307 world.step::<4>(1.0 / 60.0);
3308 }
3309
3310 let bob_pos = world.body(bob).unwrap().position;
3311 assert!(
3313 bob_pos.y < initial_y,
3314 "Bob should swing down, y={}",
3315 bob_pos.y
3316 );
3317 }
3318
3319 #[test]
3320 fn test_soft_distance_constraint() {
3321 let mut world = PhysicsWorld::<4, 4>::new();
3323 world.set_gravity(Vector3::zeros());
3324
3325 let a = world
3326 .add_body(
3327 RigidBody::new(1.0)
3328 .with_position(Vector3::new(0.0, 0.0, 0.0))
3329 .with_damping(0.0)
3330 .with_angular_damping(0.0),
3331 )
3332 .unwrap();
3333 let b = world
3334 .add_body(
3335 RigidBody::new(1.0)
3336 .with_position(Vector3::new(5.0, 0.0, 0.0))
3337 .with_damping(0.0)
3338 .with_angular_damping(0.0),
3339 )
3340 .unwrap();
3341
3342 world
3344 .add_constraint(Constraint::Distance {
3345 body_a: a,
3346 body_b: b,
3347 anchor_a: Vector3::zeros(),
3348 anchor_b: Vector3::zeros(),
3349 rest_length: 2.0,
3350 compliance: 0.01,
3351 })
3352 .unwrap();
3353
3354 for _ in 0..60 {
3356 world.step::<4>(1.0 / 60.0);
3357 }
3358
3359 let pa = world.body(a).unwrap().position;
3360 let pb = world.body(b).unwrap().position;
3361 let dist = (pb - pa).norm();
3362 assert!(dist < 5.0, "Spring should have contracted, dist={}", dist);
3364 }
3365
3366 #[test]
3367 fn test_fixed_joint_preserves_relative_orientation() {
3368 let mut world = PhysicsWorld::<4, 4>::new();
3369 world.set_gravity(Vector3::new(0.0, -10.0, 0.0));
3370
3371 let a = world
3372 .add_body(RigidBody::new_static().with_position(Vector3::new(0.0, 10.0, 0.0)))
3373 .unwrap();
3374 let b = world
3375 .add_body(
3376 RigidBody::new(1.0)
3377 .with_position(Vector3::new(2.0, 10.0, 0.0))
3378 .with_inertia_box(Vector3::new(0.5, 0.5, 0.5))
3379 .with_damping(0.0)
3380 .with_angular_damping(0.0),
3381 )
3382 .unwrap();
3383
3384 world
3386 .add_fixed_joint(a, Vector3::new(2.0, 0.0, 0.0), b, Vector3::zeros(), 0.0)
3387 .unwrap();
3388
3389 let initial_qa = world.body(a).unwrap().orientation;
3391 let initial_qb = world.body(b).unwrap().orientation;
3392 let initial_rel = initial_qb * initial_qa.inverse();
3393
3394 for _ in 0..200 {
3395 world.step::<4>(1.0 / 60.0);
3396 }
3397
3398 let qa = world.body(a).unwrap().orientation;
3400 let qb = world.body(b).unwrap().orientation;
3401 let current_rel = qb * qa.inverse();
3402 let error = current_rel * initial_rel.inverse();
3403 let angle = error.angle();
3404 assert!(angle < 0.5, "Fixed joint orientation drift: {} rad", angle);
3405 }
3406
3407 #[test]
3408 fn test_chain_of_distance_constraints() {
3409 let mut world = PhysicsWorld::<8, 8>::new();
3411 world.set_gravity(Vector3::new(0.0, -10.0, 0.0));
3412 world.solver_iterations = 8;
3413
3414 let anchor = world
3415 .add_body(RigidBody::new_static().with_position(Vector3::new(0.0, 10.0, 0.0)))
3416 .unwrap();
3417 let link1 = world
3418 .add_body(
3419 RigidBody::new(1.0)
3420 .with_position(Vector3::new(0.0, 8.0, 0.0))
3421 .with_damping(0.01)
3422 .with_angular_damping(0.01),
3423 )
3424 .unwrap();
3425 let link2 = world
3426 .add_body(
3427 RigidBody::new(1.0)
3428 .with_position(Vector3::new(0.0, 6.0, 0.0))
3429 .with_damping(0.01)
3430 .with_angular_damping(0.01),
3431 )
3432 .unwrap();
3433
3434 world
3435 .add_distance_constraint(anchor, Vector3::zeros(), link1, Vector3::zeros(), 0.0)
3436 .unwrap();
3437 world
3438 .add_distance_constraint(link1, Vector3::zeros(), link2, Vector3::zeros(), 0.0)
3439 .unwrap();
3440
3441 for _ in 0..600 {
3442 world.step::<4>(1.0 / 60.0);
3443 }
3444
3445 let pa = world.body(anchor).unwrap().position;
3447 let p1 = world.body(link1).unwrap().position;
3448 let p2 = world.body(link2).unwrap().position;
3449
3450 let d1 = (p1 - pa).norm();
3451 let d2 = (p2 - p1).norm();
3452
3453 assert!((d1 - 2.0).abs() < 0.5, "Link 1 distance: {}", d1);
3454 assert!((d2 - 2.0).abs() < 0.5, "Link 2 distance: {}", d2);
3455
3456 assert!(p1.y < pa.y, "Link 1 should be below anchor");
3458 assert!(p2.y < p1.y, "Link 2 should be below link 1");
3459 }
3460
3461 #[test]
3462 fn test_constraint_between_equal_bodies_symmetric() {
3463 let mut world = PhysicsWorld::<4, 4>::new();
3466 world.set_gravity(Vector3::zeros());
3467
3468 let a = world
3469 .add_body(
3470 RigidBody::new(1.0)
3471 .with_position(Vector3::new(-3.0, 0.0, 0.0))
3472 .with_damping(0.0)
3473 .with_angular_damping(0.0),
3474 )
3475 .unwrap();
3476 let b = world
3477 .add_body(
3478 RigidBody::new(1.0)
3479 .with_position(Vector3::new(3.0, 0.0, 0.0))
3480 .with_damping(0.0)
3481 .with_angular_damping(0.0),
3482 )
3483 .unwrap();
3484
3485 world
3487 .add_constraint(Constraint::Distance {
3488 body_a: a,
3489 body_b: b,
3490 anchor_a: Vector3::zeros(),
3491 anchor_b: Vector3::zeros(),
3492 rest_length: 2.0,
3493 compliance: 0.0,
3494 })
3495 .unwrap();
3496
3497 for _ in 0..120 {
3498 world.step::<4>(1.0 / 60.0);
3499 }
3500
3501 let pa = world.body(a).unwrap().position;
3502 let pb = world.body(b).unwrap().position;
3503
3504 let com = (pa + pb) / 2.0;
3506 assert!(
3507 com.norm() < 0.5,
3508 "COM should stay near origin, got {:?}",
3509 com
3510 );
3511 }
3512
3513 #[test]
3514 fn test_constraint_with_static_body() {
3515 let mut world = PhysicsWorld::<4, 4>::new();
3517 world.set_gravity(Vector3::zeros());
3518
3519 let fixed = world
3520 .add_body(RigidBody::new_static().with_position(Vector3::new(0.0, 0.0, 0.0)))
3521 .unwrap();
3522 let dynamic = world
3523 .add_body(
3524 RigidBody::new(1.0)
3525 .with_position(Vector3::new(5.0, 0.0, 0.0))
3526 .with_damping(0.0)
3527 .with_angular_damping(0.0),
3528 )
3529 .unwrap();
3530
3531 world
3533 .add_constraint(Constraint::Distance {
3534 body_a: fixed,
3535 body_b: dynamic,
3536 anchor_a: Vector3::zeros(),
3537 anchor_b: Vector3::zeros(),
3538 rest_length: 2.0,
3539 compliance: 0.0,
3540 })
3541 .unwrap();
3542
3543 for _ in 0..120 {
3544 world.step::<4>(1.0 / 60.0);
3545 }
3546
3547 let p_fixed = world.body(fixed).unwrap().position;
3549 assert!(approx_vec_eq(&p_fixed, &Vector3::zeros()));
3550
3551 let p_dyn = world.body(dynamic).unwrap().position;
3553 let dist = p_dyn.norm();
3554 assert!((dist - 2.0).abs() < 0.5, "Expected ~2.0, got {}", dist);
3555 }
3556
3557 #[test]
3558 fn test_solver_iterations_affect_convergence() {
3559 let make_world = |iterations: u32| -> PhysicsWorld<4, 4> {
3561 let mut world = PhysicsWorld::<4, 4>::new();
3562 world.set_gravity(Vector3::new(0.0, -10.0, 0.0));
3563 world.solver_iterations = iterations;
3564
3565 let a = world
3566 .add_body(RigidBody::new_static().with_position(Vector3::new(0.0, 10.0, 0.0)))
3567 .unwrap();
3568 let b = world
3569 .add_body(
3570 RigidBody::new(1.0)
3571 .with_position(Vector3::new(3.0, 10.0, 0.0))
3572 .with_damping(0.0)
3573 .with_angular_damping(0.0),
3574 )
3575 .unwrap();
3576
3577 world
3578 .add_distance_constraint(a, Vector3::zeros(), b, Vector3::zeros(), 0.0)
3579 .unwrap();
3580 world
3581 };
3582
3583 let mut world_low = make_world(1);
3584 let mut world_high = make_world(16);
3585
3586 for _ in 0..120 {
3587 world_low.step::<4>(1.0 / 60.0);
3588 world_high.step::<4>(1.0 / 60.0);
3589 }
3590
3591 let pa_low = world_low.body(BodyId(0)).unwrap().position;
3592 let pb_low = world_low.body(BodyId(1)).unwrap().position;
3593 let error_low = ((pb_low - pa_low).norm() - 3.0).abs();
3594
3595 let pa_high = world_high.body(BodyId(0)).unwrap().position;
3596 let pb_high = world_high.body(BodyId(1)).unwrap().position;
3597 let error_high = ((pb_high - pa_high).norm() - 3.0).abs();
3598
3599 assert!(
3601 error_high <= error_low + EPSILON,
3602 "High iterations error {} should be <= low iterations error {}",
3603 error_high,
3604 error_low
3605 );
3606 }
3607
3608 #[test]
3609 fn test_no_constraints_no_effect() {
3610 let mut world = PhysicsWorld::<4, 4>::new();
3612 world.set_gravity(Vector3::new(0.0, -10.0, 0.0));
3613
3614 let id = world
3615 .add_body(
3616 RigidBody::new(1.0)
3617 .with_position(Vector3::new(0.0, 10.0, 0.0))
3618 .with_damping(0.0),
3619 )
3620 .unwrap();
3621
3622 world.step::<4>(1.0);
3623
3624 let body = world.body(id).unwrap();
3625 assert!(approx_eq(body.velocity.y, -10.0));
3626 }
3627
3628 #[test]
3629 fn test_add_distance_constraint_helper() {
3630 let mut world = PhysicsWorld::<4, 4>::new();
3631 let a = world
3632 .add_body(RigidBody::new(1.0).with_position(Vector3::new(0.0, 0.0, 0.0)))
3633 .unwrap();
3634 let b = world
3635 .add_body(RigidBody::new(1.0).with_position(Vector3::new(5.0, 0.0, 0.0)))
3636 .unwrap();
3637
3638 let cid = world
3639 .add_distance_constraint(a, Vector3::zeros(), b, Vector3::zeros(), 0.0)
3640 .unwrap();
3641
3642 if let Constraint::Distance { rest_length, .. } = world.constraint(cid).unwrap() {
3644 assert!(approx_eq(*rest_length, 5.0));
3645 } else {
3646 panic!("Expected Distance constraint");
3647 }
3648 }
3649
3650 #[test]
3651 fn test_broad_phase_skips_distant_bodies() {
3652 let mut world = PhysicsWorld::<4>::new();
3654 world
3655 .add_body(
3656 RigidBody::new(1.0)
3657 .with_position(Vector3::new(0.0, 0.0, 0.0))
3658 .with_collider(Collider::Sphere { radius: 1.0 }),
3659 )
3660 .unwrap();
3661 world
3662 .add_body(
3663 RigidBody::new(1.0)
3664 .with_position(Vector3::new(100.0, 0.0, 0.0))
3665 .with_collider(Collider::Sphere { radius: 1.0 }),
3666 )
3667 .unwrap();
3668
3669 let contacts = world.detect_collisions::<4>();
3670 assert!(contacts.is_empty(), "Distant bodies should not collide");
3671 }
3672
3673 #[test]
3674 fn test_broad_phase_regression() {
3675 let mut world = PhysicsWorld::<4>::new();
3677 world
3678 .add_body(
3679 RigidBody::new(1.0)
3680 .with_position(Vector3::new(0.0, 0.0, 0.0))
3681 .with_collider(Collider::Sphere { radius: 1.0 }),
3682 )
3683 .unwrap();
3684 world
3685 .add_body(
3686 RigidBody::new(1.0)
3687 .with_position(Vector3::new(1.5, 0.0, 0.0))
3688 .with_collider(Collider::Sphere { radius: 1.0 }),
3689 )
3690 .unwrap();
3691
3692 let contacts = world.detect_collisions::<4>();
3693 assert_eq!(
3694 contacts.len(),
3695 1,
3696 "Overlapping spheres should produce one contact"
3697 );
3698 assert!(contacts[0].penetration > 0.0);
3699 }
3700}