1use crate::body::{BodyHandle, BodyType, RigidBody};
15use nalgebra::SVector;
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
19pub struct BroadphasePair(pub BodyHandle, pub BodyHandle);
20
21#[derive(Clone, Debug)]
23pub struct Aabb<const D: usize> {
24 pub min: SVector<f64, D>,
25 pub max: SVector<f64, D>,
26}
27
28impl<const D: usize> Aabb<D> {
29 pub fn empty() -> Self {
30 Self {
31 min: SVector::from_element(f64::MAX),
32 max: SVector::from_element(f64::MIN),
33 }
34 }
35 pub fn union(&self, other: &Self) -> Self {
36 let mut min = self.min;
37 let mut max = self.max;
38 for i in 0..D {
39 if other.min[i] < min[i] {
40 min[i] = other.min[i];
41 }
42 if other.max[i] > max[i] {
43 max[i] = other.max[i];
44 }
45 }
46 Self { min, max }
47 }
48 pub fn overlaps(&self, other: &Self) -> bool {
49 for i in 0..D {
50 if self.max[i] < other.min[i] || self.min[i] > other.max[i] {
51 return false;
52 }
53 }
54 true
55 }
56 pub fn from_sphere(center: &SVector<f64, D>, radius: f64) -> Self {
57 Self {
58 min: center - SVector::from_element(radius),
59 max: center + SVector::from_element(radius),
60 }
61 }
62}
63
64fn quantize(value: f64, world_min: f64, world_max: f64, bits: usize) -> u32 {
66 let range = world_max - world_min;
67 if range <= 0.0 {
68 return 0;
69 }
70 let max_val = ((1u64 << bits) - 1) as f64;
71 let normalized = ((value - world_min) / range).clamp(0.0, 1.0);
72 (normalized * max_val) as u32
73}
74
75pub fn morton_encode<const D: usize>(
77 position: &SVector<f64, D>,
78 world_min: &SVector<f64, D>,
79 world_max: &SVector<f64, D>,
80) -> u64 {
81 let bits = 64 / D;
82 let mut code: u64 = 0;
83 for bit in 0..bits {
84 for axis in 0..D {
85 let q = quantize(position[axis], world_min[axis], world_max[axis], bits);
86 code |= (((q >> bit) & 1) as u64) << (bit * D + axis);
87 }
88 }
89 code
90}
91
92pub fn morton_prefix(code: u64, prefix_bits: usize) -> u64 {
94 if prefix_bits >= 64 {
95 code
96 } else {
97 code >> (64 - prefix_bits)
98 }
99}
100
101const BRUTE_FORCE_THRESHOLD: usize = 50;
103const LEAF_BIT: u32 = 0x8000_0000;
104fn is_leaf(idx: u32) -> bool {
105 idx & LEAF_BIT != 0
106}
107fn leaf_index(idx: u32) -> usize {
108 (idx & !LEAF_BIT) as usize
109}
110
111#[derive(Clone, Debug)]
112struct BvhNode<const D: usize> {
113 aabb: Aabb<D>,
114 left: u32,
115 right: u32,
116}
117
118pub struct Lbvh<const D: usize> {
120 sorted_indices: Vec<usize>,
121 leaf_aabbs: Vec<Aabb<D>>,
122 nodes: Vec<BvhNode<D>>,
123}
124
125impl<const D: usize> Lbvh<D> {
126 pub fn build(bodies: &[RigidBody<D>]) -> Self {
127 let n = bodies.len();
128 let mut aabbs = Vec::with_capacity(n);
129 let mut world_min = SVector::<f64, D>::from_element(f64::MAX);
130 let mut world_max = SVector::<f64, D>::from_element(f64::MIN);
131 for body in bodies {
132 let (c_local, r) = body.collider.bounding_sphere();
133 let c_world = body.transform.transform_point(&c_local).0;
134 let aabb = Aabb::from_sphere(&c_world, r);
135 for i in 0..D {
136 if aabb.min[i] < world_min[i] {
137 world_min[i] = aabb.min[i];
138 }
139 if aabb.max[i] > world_max[i] {
140 world_max[i] = aabb.max[i];
141 }
142 }
143 aabbs.push(aabb);
144 }
145 for i in 0..D {
146 if world_max[i] - world_min[i] < 1e-6 {
147 world_min[i] -= 1.0;
148 world_max[i] += 1.0;
149 }
150 }
151 let morton_codes: Vec<u64> = aabbs
152 .iter()
153 .map(|a| {
154 let c = (a.min + a.max) * 0.5;
155 morton_encode::<D>(&c, &world_min, &world_max)
156 })
157 .collect();
158 let mut sorted_indices: Vec<usize> = (0..n).collect();
159 sorted_indices.sort_by_key(|&i| morton_codes[i]);
160 let sorted_aabbs: Vec<Aabb<D>> = sorted_indices.iter().map(|&i| aabbs[i].clone()).collect();
161 let nodes = if n <= 1 {
162 Vec::new()
163 } else {
164 Self::build_nodes(
165 &sorted_indices
166 .iter()
167 .map(|&i| morton_codes[i])
168 .collect::<Vec<_>>(),
169 &sorted_aabbs,
170 )
171 };
172 Self {
173 sorted_indices,
174 leaf_aabbs: sorted_aabbs,
175 nodes,
176 }
177 }
178
179 fn find_split(codes: &[u64], first: usize, last: usize) -> usize {
180 if codes[first] == codes[last] {
181 return (first + last) / 2;
182 }
183 let cp = (codes[first] ^ codes[last]).leading_zeros();
184 let mut split = first;
185 let mut step = last - first;
186 loop {
187 step = step.div_ceil(2);
188 let ns = split + step;
189 if ns < last && (codes[first] ^ codes[ns]).leading_zeros() > cp {
190 split = ns;
191 }
192 if step <= 1 {
193 break;
194 }
195 }
196 split
197 }
198
199 fn build_nodes(codes: &[u64], aabbs: &[Aabb<D>]) -> Vec<BvhNode<D>> {
200 let mut nodes = Vec::with_capacity(codes.len() - 1);
201 Self::build_rec(codes, aabbs, 0, codes.len() - 1, &mut nodes);
202 nodes
203 }
204
205 fn build_rec(
206 codes: &[u64],
207 aabbs: &[Aabb<D>],
208 first: usize,
209 last: usize,
210 nodes: &mut Vec<BvhNode<D>>,
211 ) -> u32 {
212 if first == last {
213 return LEAF_BIT | (first as u32);
214 }
215 let split = Self::find_split(codes, first, last);
216 let left = Self::build_rec(codes, aabbs, first, split, nodes);
217 let right = Self::build_rec(codes, aabbs, split + 1, last, nodes);
218 let la = if is_leaf(left) {
219 aabbs[leaf_index(left)].clone()
220 } else {
221 nodes[left as usize].aabb.clone()
222 };
223 let ra = if is_leaf(right) {
224 aabbs[leaf_index(right)].clone()
225 } else {
226 nodes[right as usize].aabb.clone()
227 };
228 let idx = nodes.len() as u32;
229 nodes.push(BvhNode {
230 aabb: la.union(&ra),
231 left,
232 right,
233 });
234 idx
235 }
236
237 pub fn query_pairs(&self, bodies: &[RigidBody<D>]) -> Vec<BroadphasePair> {
238 let n = self.sorted_indices.len();
239 if n <= 1 {
240 return Vec::new();
241 }
242 let mut pairs = Vec::new();
243 let root = (self.nodes.len() - 1) as u32;
244 for li in 0..n {
245 let la = &self.leaf_aabbs[li];
246 let lt = bodies[self.sorted_indices[li]].body_type;
247 let mut stack = arrayvec::ArrayVec::<u32, 64>::new();
248 stack.push(root);
249 while let Some(ni) = stack.pop() {
250 if is_leaf(ni) {
251 let oi = leaf_index(ni);
252 if oi > li {
253 let ot = bodies[self.sorted_indices[oi]].body_type;
254 if lt == BodyType::Static && ot == BodyType::Static {
255 continue;
256 }
257 if !should_collide(
258 &bodies[self.sorted_indices[li]],
259 &bodies[self.sorted_indices[oi]],
260 ) {
261 continue;
262 }
263 if la.overlaps(&self.leaf_aabbs[oi]) {
264 let (ha, hb) = (
265 bodies[self.sorted_indices[li]].handle,
266 bodies[self.sorted_indices[oi]].handle,
267 );
268 if ha < hb {
269 pairs.push(BroadphasePair(ha, hb));
270 } else {
271 pairs.push(BroadphasePair(hb, ha));
272 }
273 }
274 }
275 continue;
276 }
277 let node = &self.nodes[ni as usize];
278 if la.overlaps(&node.aabb) {
279 stack.push(node.left);
280 stack.push(node.right);
281 }
282 }
283 }
284 pairs.sort_unstable_by_key(|p| (p.0, p.1));
285 pairs.dedup();
286 pairs
287 }
288}
289
290#[inline]
293fn should_collide<const D: usize>(a: &RigidBody<D>, b: &RigidBody<D>) -> bool {
294 (a.collision_group & b.collision_mask != 0) && (b.collision_group & a.collision_mask != 0)
295}
296
297pub fn find_pairs<const D: usize>(bodies: &[RigidBody<D>]) -> Vec<BroadphasePair> {
299 if bodies.len() < BRUTE_FORCE_THRESHOLD {
300 find_pairs_brute(bodies)
301 } else {
302 Lbvh::build(bodies).query_pairs(bodies)
303 }
304}
305
306#[derive(Clone, Debug)]
316pub struct BpEntry<const D: usize> {
317 pub handle: BodyHandle,
318 pub aabb: Aabb<D>,
319 pub sphere_center: SVector<f64, D>,
321 pub sphere_radius: f64,
323 pub body_type: BodyType,
324 pub collision_group: u32,
325 pub collision_mask: u32,
326}
327
328impl<const D: usize> BpEntry<D> {
329 pub fn from_body(body: &RigidBody<D>) -> Self {
330 let (c_local, r) = body.collider.bounding_sphere();
331 let c_world = body.transform.transform_point(&c_local).0;
332 BpEntry {
333 handle: body.handle,
334 aabb: Aabb::from_sphere(&c_world, r),
335 sphere_center: c_world,
336 sphere_radius: r,
337 body_type: body.body_type,
338 collision_group: body.collision_group,
339 collision_mask: body.collision_mask,
340 }
341 }
342}
343
344pub struct StaticBroadphase<const D: usize> {
353 entries: Vec<BpEntry<D>>,
354}
355
356impl<const D: usize> StaticBroadphase<D> {
357 pub fn new() -> Self {
358 Self {
359 entries: Vec::new(),
360 }
361 }
362
363 pub fn rebuild(&mut self, bodies: &[RigidBody<D>]) {
367 self.entries = bodies
368 .iter()
369 .filter(|b| b.body_type == BodyType::Static || b.body_type == BodyType::Kinematic)
370 .map(BpEntry::from_body)
371 .collect();
372 }
373
374 pub fn is_empty(&self) -> bool {
375 self.entries.is_empty()
376 }
377
378 pub fn len(&self) -> usize {
379 self.entries.len()
380 }
381
382 fn query_against<'a>(
386 &'a self,
387 dyn_entry: &'a BpEntry<D>,
388 ) -> impl Iterator<Item = BroadphasePair> + 'a {
389 self.entries.iter().filter_map(move |s| {
390 let group_ok = (dyn_entry.collision_group & s.collision_mask != 0)
391 && (s.collision_group & dyn_entry.collision_mask != 0);
392 if group_ok {
393 let dist = (dyn_entry.sphere_center - s.sphere_center).norm();
394 if dist <= dyn_entry.sphere_radius + s.sphere_radius {
395 let (ha, hb) = (dyn_entry.handle, s.handle);
396 let pair = if ha < hb {
397 BroadphasePair(ha, hb)
398 } else {
399 BroadphasePair(hb, ha)
400 };
401 return Some(pair);
402 }
403 }
404 None
405 })
406 }
407}
408
409impl<const D: usize> Default for StaticBroadphase<D> {
410 fn default() -> Self {
411 Self::new()
412 }
413}
414
415pub fn find_pairs_incremental<const D: usize>(
428 bodies: &[RigidBody<D>],
429 static_bp: &StaticBroadphase<D>,
430) -> Vec<BroadphasePair> {
431 if bodies.is_empty() {
433 return Vec::new();
434 }
435 if static_bp.is_empty() {
436 return find_pairs(bodies);
438 }
439
440 let dynamic: Vec<BpEntry<D>> = bodies
442 .iter()
443 .filter(|b| b.body_type == BodyType::Dynamic)
444 .map(BpEntry::from_body)
445 .collect();
446
447 if dynamic.len() >= BRUTE_FORCE_THRESHOLD {
450 return find_pairs(bodies);
451 }
452
453 let mut pairs = Vec::new();
454
455 for i in 0..dynamic.len() {
457 for j in (i + 1)..dynamic.len() {
458 let a = &dynamic[i];
459 let b = &dynamic[j];
460 let group_ok = (a.collision_group & b.collision_mask != 0)
461 && (b.collision_group & a.collision_mask != 0);
462 if group_ok {
463 let dist = (a.sphere_center - b.sphere_center).norm();
464 if dist <= a.sphere_radius + b.sphere_radius {
465 let (ha, hb) = (a.handle, b.handle);
466 if ha < hb {
467 pairs.push(BroadphasePair(ha, hb));
468 } else {
469 pairs.push(BroadphasePair(hb, ha));
470 }
471 }
472 }
473 }
474 }
475
476 for dyn_entry in &dynamic {
478 pairs.extend(static_bp.query_against(dyn_entry));
479 }
480
481 pairs.sort_unstable_by_key(|p| (p.0, p.1));
482 pairs.dedup();
483 pairs
484}
485
486fn find_pairs_brute<const D: usize>(bodies: &[RigidBody<D>]) -> Vec<BroadphasePair> {
487 let mut pairs = Vec::new();
488 for i in 0..bodies.len() {
489 let ti = bodies[i].body_type;
490 for j in (i + 1)..bodies.len() {
491 if ti == BodyType::Static && bodies[j].body_type == BodyType::Static {
492 continue;
493 }
494 if !should_collide(&bodies[i], &bodies[j]) {
495 continue;
496 }
497 let (ci, ri) = bodies[i].collider.bounding_sphere();
498 let (cj, rj) = bodies[j].collider.bounding_sphere();
499 let wi = bodies[i].transform.transform_point(&ci);
500 let wj = bodies[j].transform.transform_point(&cj);
501 if wi.distance(&wj) <= ri + rj {
502 pairs.push(BroadphasePair(bodies[i].handle, bodies[j].handle));
503 }
504 }
505 }
506 pairs
507}
508
509#[cfg(test)]
510mod tests {
511 use super::*;
512 use crate::body::RigidBody;
513 use symtropy_math::{Point, Sphere};
514
515 #[test]
516 fn overlapping_pair_detected() {
517 let bodies = vec![
518 RigidBody::<3>::dynamic_sphere(BodyHandle(0), Point::new([0.0, 0.0, 0.0]), 1.0, 1.0),
519 RigidBody::<3>::dynamic_sphere(BodyHandle(1), Point::new([1.5, 0.0, 0.0]), 1.0, 1.0),
520 ];
521 assert_eq!(find_pairs(&bodies).len(), 1);
522 }
523
524 #[test]
525 fn separated_pair_not_detected() {
526 let bodies = vec![
527 RigidBody::<3>::dynamic_sphere(BodyHandle(0), Point::new([0.0, 0.0, 0.0]), 1.0, 1.0),
528 RigidBody::<3>::dynamic_sphere(BodyHandle(1), Point::new([5.0, 0.0, 0.0]), 1.0, 1.0),
529 ];
530 assert!(find_pairs(&bodies).is_empty());
531 }
532
533 #[test]
534 fn static_static_ignored() {
535 let bodies = vec![
536 RigidBody::<3>::static_body(BodyHandle(0), Point::origin(), Box::new(Sphere::unit())),
537 RigidBody::<3>::static_body(
538 BodyHandle(1),
539 Point::new([0.5, 0.0, 0.0]),
540 Box::new(Sphere::unit()),
541 ),
542 ];
543 assert!(find_pairs(&bodies).is_empty());
544 }
545
546 #[test]
547 fn static_dynamic_detected() {
548 let bodies = vec![
549 RigidBody::<3>::static_body(BodyHandle(0), Point::origin(), Box::new(Sphere::unit())),
550 RigidBody::<3>::dynamic_sphere(BodyHandle(1), Point::new([1.5, 0.0, 0.0]), 1.0, 1.0),
551 ];
552 assert_eq!(find_pairs(&bodies).len(), 1);
553 }
554
555 #[test]
556 fn multiple_pairs() {
557 let bodies = vec![
558 RigidBody::<3>::dynamic_sphere(BodyHandle(0), Point::new([0.0, 0.0, 0.0]), 1.0, 1.0),
559 RigidBody::<3>::dynamic_sphere(BodyHandle(1), Point::new([1.5, 0.0, 0.0]), 1.0, 1.0),
560 RigidBody::<3>::dynamic_sphere(BodyHandle(2), Point::new([10.0, 0.0, 0.0]), 1.0, 1.0),
561 RigidBody::<3>::dynamic_sphere(BodyHandle(3), Point::new([1.0, 1.0, 0.0]), 1.0, 1.0),
562 ];
563 assert!(find_pairs(&bodies).len() >= 2);
564 }
565
566 #[test]
567 fn morton_encode_2d_origin() {
568 let pos = SVector::<f64, 2>::from([0.0, 0.0]);
569 let min = SVector::<f64, 2>::from([0.0, 0.0]);
570 let max = SVector::<f64, 2>::from([100.0, 100.0]);
571 assert_eq!(morton_encode::<2>(&pos, &min, &max), 0);
572 }
573
574 #[test]
575 fn morton_locality() {
576 let min = SVector::<f64, 3>::from([0.0; 3]);
577 let max = SVector::<f64, 3>::from([100.0; 3]);
578 let a = morton_encode::<3>(&SVector::from([10.0, 10.0, 10.0]), &min, &max);
579 let b = morton_encode::<3>(&SVector::from([11.0, 10.0, 10.0]), &min, &max);
580 let far = morton_encode::<3>(&SVector::from([90.0, 90.0, 90.0]), &min, &max);
581 assert!((a as i64 - b as i64).unsigned_abs() < (a as i64 - far as i64).unsigned_abs());
582 }
583
584 #[test]
585 fn lbvh_matches_brute_force() {
586 let mut bodies = Vec::new();
587 for i in 0..(BRUTE_FORCE_THRESHOLD + 10) {
588 let x = (i as f64) * 0.5;
589 bodies.push(RigidBody::<2>::dynamic_sphere(
590 BodyHandle(i),
591 Point::new([x, 0.0]),
592 1.0,
593 1.0,
594 ));
595 }
596 let mut brute = find_pairs_brute(&bodies);
597 brute.sort_by_key(|p| (p.0, p.1));
598 let lbvh = find_pairs(&bodies);
599 for pair in &brute {
600 assert!(lbvh.contains(pair), "LBVH missing pair {:?}", pair);
601 }
602 }
603
604 #[test]
605 fn lbvh_deterministic() {
606 let bodies = vec![
607 RigidBody::<3>::dynamic_sphere(BodyHandle(0), Point::new([0.0, 0.0, 0.0]), 1.0, 1.0),
608 RigidBody::<3>::dynamic_sphere(BodyHandle(1), Point::new([1.5, 0.0, 0.0]), 1.0, 1.0),
609 RigidBody::<3>::dynamic_sphere(BodyHandle(2), Point::new([0.5, 0.5, 0.5]), 0.8, 1.0),
610 ];
611 assert_eq!(find_pairs(&bodies), find_pairs(&bodies));
612 }
613
614 #[test]
615 fn aabb_overlap() {
616 let a = Aabb::<2> {
617 min: SVector::from([0.0, 0.0]),
618 max: SVector::from([2.0, 2.0]),
619 };
620 let b = Aabb::<2> {
621 min: SVector::from([1.0, 1.0]),
622 max: SVector::from([3.0, 3.0]),
623 };
624 let c = Aabb::<2> {
625 min: SVector::from([5.0, 5.0]),
626 max: SVector::from([6.0, 6.0]),
627 };
628 assert!(a.overlaps(&b));
629 assert!(!a.overlaps(&c));
630 }
631
632 #[test]
635 fn incremental_dynamic_static_detected() {
636 let bodies = vec![
637 RigidBody::<3>::static_body(BodyHandle(0), Point::origin(), Box::new(Sphere::unit())),
638 RigidBody::<3>::dynamic_sphere(BodyHandle(1), Point::new([1.5, 0.0, 0.0]), 1.0, 1.0),
639 ];
640 let mut static_bp = super::StaticBroadphase::new();
641 static_bp.rebuild(&bodies);
642
643 let pairs = super::find_pairs_incremental(&bodies, &static_bp);
644 assert_eq!(pairs.len(), 1);
645 let p = pairs[0];
646 assert!(
647 (p.0 == BodyHandle(0) && p.1 == BodyHandle(1))
648 || (p.0 == BodyHandle(1) && p.1 == BodyHandle(0))
649 );
650 }
651
652 #[test]
653 fn incremental_static_static_not_produced() {
654 let bodies = vec![
655 RigidBody::<3>::static_body(BodyHandle(0), Point::origin(), Box::new(Sphere::unit())),
656 RigidBody::<3>::static_body(
657 BodyHandle(1),
658 Point::new([0.5, 0.0, 0.0]),
659 Box::new(Sphere::unit()),
660 ),
661 ];
662 let mut static_bp = super::StaticBroadphase::new();
663 static_bp.rebuild(&bodies);
664
665 let pairs = super::find_pairs_incremental(&bodies, &static_bp);
666 assert!(pairs.is_empty(), "static–static must never collide");
667 }
668
669 #[test]
670 fn incremental_matches_standard_find_pairs() {
671 let bodies = vec![
672 RigidBody::<3>::static_body(
673 BodyHandle(0),
674 Point::new([0.0, 0.0, 0.0]),
675 Box::new(Sphere::unit()),
676 ),
677 RigidBody::<3>::dynamic_sphere(BodyHandle(1), Point::new([1.5, 0.0, 0.0]), 1.0, 1.0),
678 RigidBody::<3>::dynamic_sphere(BodyHandle(2), Point::new([0.0, 1.5, 0.0]), 1.0, 1.0),
679 RigidBody::<3>::dynamic_sphere(BodyHandle(3), Point::new([10.0, 0.0, 0.0]), 1.0, 1.0),
680 ];
681 let mut static_bp = super::StaticBroadphase::new();
682 static_bp.rebuild(&bodies);
683
684 let mut incremental = super::find_pairs_incremental(&bodies, &static_bp);
685 let mut standard = find_pairs(&bodies);
686 incremental.sort_by_key(|p| (p.0, p.1));
687 standard.sort_by_key(|p| (p.0, p.1));
688 assert_eq!(
689 incremental, standard,
690 "incremental must match standard broadphase"
691 );
692 }
693
694 #[test]
695 fn incremental_no_static_degrades_to_standard() {
696 let bodies = vec![
697 RigidBody::<3>::dynamic_sphere(BodyHandle(0), Point::new([0.0, 0.0, 0.0]), 1.0, 1.0),
698 RigidBody::<3>::dynamic_sphere(BodyHandle(1), Point::new([1.5, 0.0, 0.0]), 1.0, 1.0),
699 ];
700 let empty_bp = super::StaticBroadphase::<3>::new(); let pairs = super::find_pairs_incremental(&bodies, &empty_bp);
702 assert_eq!(pairs.len(), 1);
703 }
704
705 #[test]
706 fn static_broadphase_rebuild_updates_cache() {
707 let bodies = vec![
708 RigidBody::<3>::static_body(BodyHandle(0), Point::origin(), Box::new(Sphere::unit())),
709 RigidBody::<3>::dynamic_sphere(BodyHandle(1), Point::new([1.5, 0.0, 0.0]), 1.0, 1.0),
710 ];
711 let mut static_bp = super::StaticBroadphase::<3>::new();
712 assert_eq!(static_bp.len(), 0);
713
714 static_bp.rebuild(&bodies);
715 assert_eq!(static_bp.len(), 1, "should cache the one static body");
716 }
717}