1use nalgebra::SVector;
15use crate::body::{BodyHandle, BodyType, RigidBody};
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 { min: SVector::from_element(f64::MAX), max: SVector::from_element(f64::MIN) }
31 }
32 pub fn union(&self, other: &Self) -> Self {
33 let mut min = self.min; let mut max = self.max;
34 for i in 0..D {
35 if other.min[i] < min[i] { min[i] = other.min[i]; }
36 if other.max[i] > max[i] { max[i] = other.max[i]; }
37 }
38 Self { min, max }
39 }
40 pub fn overlaps(&self, other: &Self) -> bool {
41 for i in 0..D {
42 if self.max[i] < other.min[i] || self.min[i] > other.max[i] { return false; }
43 }
44 true
45 }
46 pub fn from_sphere(center: &SVector<f64, D>, radius: f64) -> Self {
47 Self { min: center - SVector::from_element(radius), max: center + SVector::from_element(radius) }
48 }
49}
50
51fn quantize(value: f64, world_min: f64, world_max: f64, bits: usize) -> u32 {
53 let range = world_max - world_min;
54 if range <= 0.0 { return 0; }
55 let max_val = ((1u64 << bits) - 1) as f64;
56 let normalized = ((value - world_min) / range).clamp(0.0, 1.0);
57 (normalized * max_val) as u32
58}
59
60pub fn morton_encode<const D: usize>(
62 position: &SVector<f64, D>, world_min: &SVector<f64, D>, world_max: &SVector<f64, D>,
63) -> u64 {
64 let bits = 64 / D;
65 let mut code: u64 = 0;
66 for bit in 0..bits {
67 for axis in 0..D {
68 let q = quantize(position[axis], world_min[axis], world_max[axis], bits);
69 code |= (((q >> bit) & 1) as u64) << (bit * D + axis);
70 }
71 }
72 code
73}
74
75pub fn morton_prefix(code: u64, prefix_bits: usize) -> u64 {
77 if prefix_bits >= 64 { code } else { code >> (64 - prefix_bits) }
78}
79
80const BRUTE_FORCE_THRESHOLD: usize = 50;
82const LEAF_BIT: u32 = 0x8000_0000;
83fn is_leaf(idx: u32) -> bool { idx & LEAF_BIT != 0 }
84fn leaf_index(idx: u32) -> usize { (idx & !LEAF_BIT) as usize }
85
86#[derive(Clone, Debug)]
87struct BvhNode<const D: usize> { aabb: Aabb<D>, left: u32, right: u32 }
88
89pub struct Lbvh<const D: usize> {
91 sorted_indices: Vec<usize>,
92 leaf_aabbs: Vec<Aabb<D>>,
93 nodes: Vec<BvhNode<D>>,
94}
95
96impl<const D: usize> Lbvh<D> {
97 pub fn build(bodies: &[RigidBody<D>]) -> Self {
98 let n = bodies.len();
99 let mut aabbs = Vec::with_capacity(n);
100 let mut world_min = SVector::<f64, D>::from_element(f64::MAX);
101 let mut world_max = SVector::<f64, D>::from_element(f64::MIN);
102 for body in bodies {
103 let (c_local, r) = body.collider.bounding_sphere();
104 let c_world = body.transform.transform_point(&c_local).0;
105 let aabb = Aabb::from_sphere(&c_world, r);
106 for i in 0..D {
107 if aabb.min[i] < world_min[i] { world_min[i] = aabb.min[i]; }
108 if aabb.max[i] > world_max[i] { world_max[i] = aabb.max[i]; }
109 }
110 aabbs.push(aabb);
111 }
112 for i in 0..D {
113 if world_max[i] - world_min[i] < 1e-6 { world_min[i] -= 1.0; world_max[i] += 1.0; }
114 }
115 let morton_codes: Vec<u64> = aabbs.iter()
116 .map(|a| { let c = (a.min + a.max) * 0.5; morton_encode::<D>(&c, &world_min, &world_max) })
117 .collect();
118 let mut sorted_indices: Vec<usize> = (0..n).collect();
119 sorted_indices.sort_by_key(|&i| morton_codes[i]);
120 let sorted_aabbs: Vec<Aabb<D>> = sorted_indices.iter().map(|&i| aabbs[i].clone()).collect();
121 let nodes = if n <= 1 { Vec::new() } else { Self::build_nodes(&sorted_indices.iter().map(|&i| morton_codes[i]).collect::<Vec<_>>(), &sorted_aabbs) };
122 Self { sorted_indices, leaf_aabbs: sorted_aabbs, nodes }
123 }
124
125 fn find_split(codes: &[u64], first: usize, last: usize) -> usize {
126 if codes[first] == codes[last] { return (first + last) / 2; }
127 let cp = (codes[first] ^ codes[last]).leading_zeros();
128 let mut split = first; let mut step = last - first;
129 loop {
130 step = (step + 1) / 2;
131 let ns = split + step;
132 if ns < last && (codes[first] ^ codes[ns]).leading_zeros() > cp { split = ns; }
133 if step <= 1 { break; }
134 }
135 split
136 }
137
138 fn build_nodes(codes: &[u64], aabbs: &[Aabb<D>]) -> Vec<BvhNode<D>> {
139 let mut nodes = Vec::with_capacity(codes.len() - 1);
140 Self::build_rec(codes, aabbs, 0, codes.len() - 1, &mut nodes);
141 nodes
142 }
143
144 fn build_rec(codes: &[u64], aabbs: &[Aabb<D>], first: usize, last: usize, nodes: &mut Vec<BvhNode<D>>) -> u32 {
145 if first == last { return LEAF_BIT | (first as u32); }
146 let split = Self::find_split(codes, first, last);
147 let left = Self::build_rec(codes, aabbs, first, split, nodes);
148 let right = Self::build_rec(codes, aabbs, split + 1, last, nodes);
149 let la = if is_leaf(left) { aabbs[leaf_index(left)].clone() } else { nodes[left as usize].aabb.clone() };
150 let ra = if is_leaf(right) { aabbs[leaf_index(right)].clone() } else { nodes[right as usize].aabb.clone() };
151 let idx = nodes.len() as u32;
152 nodes.push(BvhNode { aabb: la.union(&ra), left, right });
153 idx
154 }
155
156 pub fn query_pairs(&self, bodies: &[RigidBody<D>]) -> Vec<BroadphasePair> {
157 let n = self.sorted_indices.len();
158 if n <= 1 { return Vec::new(); }
159 let mut pairs = Vec::new();
160 let root = (self.nodes.len() - 1) as u32;
161 for li in 0..n {
162 let la = &self.leaf_aabbs[li];
163 let lt = bodies[self.sorted_indices[li]].body_type;
164 let mut stack = arrayvec::ArrayVec::<u32, 64>::new();
165 stack.push(root);
166 while let Some(ni) = stack.pop() {
167 if is_leaf(ni) {
168 let oi = leaf_index(ni);
169 if oi > li {
170 let ot = bodies[self.sorted_indices[oi]].body_type;
171 if lt == BodyType::Static && ot == BodyType::Static { continue; }
172 if !should_collide(&bodies[self.sorted_indices[li]], &bodies[self.sorted_indices[oi]]) { continue; }
173 if la.overlaps(&self.leaf_aabbs[oi]) {
174 let (ha, hb) = (bodies[self.sorted_indices[li]].handle, bodies[self.sorted_indices[oi]].handle);
175 if ha < hb { pairs.push(BroadphasePair(ha, hb)); }
176 else { pairs.push(BroadphasePair(hb, ha)); }
177 }
178 }
179 continue;
180 }
181 let node = &self.nodes[ni as usize];
182 if la.overlaps(&node.aabb) { stack.push(node.left); stack.push(node.right); }
183 }
184 }
185 pairs.sort_unstable_by_key(|p| (p.0, p.1));
186 pairs.dedup();
187 pairs
188 }
189}
190
191#[inline]
194fn should_collide<const D: usize>(a: &RigidBody<D>, b: &RigidBody<D>) -> bool {
195 (a.collision_group & b.collision_mask != 0) && (b.collision_group & a.collision_mask != 0)
196}
197
198pub fn find_pairs<const D: usize>(bodies: &[RigidBody<D>]) -> Vec<BroadphasePair> {
200 if bodies.len() < BRUTE_FORCE_THRESHOLD { find_pairs_brute(bodies) }
201 else { Lbvh::build(bodies).query_pairs(bodies) }
202}
203
204#[derive(Clone, Debug)]
214pub struct BpEntry<const D: usize> {
215 pub handle: BodyHandle,
216 pub aabb: Aabb<D>,
217 pub sphere_center: SVector<f64, D>,
219 pub sphere_radius: f64,
221 pub body_type: BodyType,
222 pub collision_group: u32,
223 pub collision_mask: u32,
224}
225
226impl<const D: usize> BpEntry<D> {
227 pub fn from_body(body: &RigidBody<D>) -> Self {
228 let (c_local, r) = body.collider.bounding_sphere();
229 let c_world = body.transform.transform_point(&c_local).0;
230 BpEntry {
231 handle: body.handle,
232 aabb: Aabb::from_sphere(&c_world, r),
233 sphere_center: c_world,
234 sphere_radius: r,
235 body_type: body.body_type,
236 collision_group: body.collision_group,
237 collision_mask: body.collision_mask,
238 }
239 }
240}
241
242pub struct StaticBroadphase<const D: usize> {
251 entries: Vec<BpEntry<D>>,
252}
253
254impl<const D: usize> StaticBroadphase<D> {
255 pub fn new() -> Self {
256 Self { entries: Vec::new() }
257 }
258
259 pub fn rebuild(&mut self, bodies: &[RigidBody<D>]) {
263 self.entries = bodies
264 .iter()
265 .filter(|b| b.body_type == BodyType::Static || b.body_type == BodyType::Kinematic)
266 .map(BpEntry::from_body)
267 .collect();
268 }
269
270 pub fn is_empty(&self) -> bool {
271 self.entries.is_empty()
272 }
273
274 pub fn len(&self) -> usize {
275 self.entries.len()
276 }
277
278 fn query_against<'a>(
282 &'a self,
283 dyn_entry: &'a BpEntry<D>,
284 ) -> impl Iterator<Item = BroadphasePair> + 'a {
285 self.entries.iter().filter_map(move |s| {
286 let group_ok = (dyn_entry.collision_group & s.collision_mask != 0)
287 && (s.collision_group & dyn_entry.collision_mask != 0);
288 if group_ok {
289 let dist = (dyn_entry.sphere_center - s.sphere_center).norm();
290 if dist <= dyn_entry.sphere_radius + s.sphere_radius {
291 let (ha, hb) = (dyn_entry.handle, s.handle);
292 let pair = if ha < hb {
293 BroadphasePair(ha, hb)
294 } else {
295 BroadphasePair(hb, ha)
296 };
297 return Some(pair);
298 }
299 }
300 None
301 })
302 }
303}
304
305impl<const D: usize> Default for StaticBroadphase<D> {
306 fn default() -> Self {
307 Self::new()
308 }
309}
310
311pub fn find_pairs_incremental<const D: usize>(
324 bodies: &[RigidBody<D>],
325 static_bp: &StaticBroadphase<D>,
326) -> Vec<BroadphasePair> {
327 if bodies.is_empty() {
329 return Vec::new();
330 }
331 if static_bp.is_empty() {
332 return find_pairs(bodies);
334 }
335
336 let dynamic: Vec<BpEntry<D>> = bodies
338 .iter()
339 .filter(|b| b.body_type == BodyType::Dynamic)
340 .map(BpEntry::from_body)
341 .collect();
342
343 if dynamic.len() >= BRUTE_FORCE_THRESHOLD {
346 return find_pairs(bodies);
347 }
348
349 let mut pairs = Vec::new();
350
351 for i in 0..dynamic.len() {
353 for j in (i + 1)..dynamic.len() {
354 let a = &dynamic[i];
355 let b = &dynamic[j];
356 let group_ok = (a.collision_group & b.collision_mask != 0)
357 && (b.collision_group & a.collision_mask != 0);
358 if group_ok {
359 let dist = (a.sphere_center - b.sphere_center).norm();
360 if dist <= a.sphere_radius + b.sphere_radius {
361 let (ha, hb) = (a.handle, b.handle);
362 if ha < hb {
363 pairs.push(BroadphasePair(ha, hb));
364 } else {
365 pairs.push(BroadphasePair(hb, ha));
366 }
367 }
368 }
369 }
370 }
371
372 for dyn_entry in &dynamic {
374 pairs.extend(static_bp.query_against(dyn_entry));
375 }
376
377 pairs.sort_unstable_by_key(|p| (p.0, p.1));
378 pairs.dedup();
379 pairs
380}
381
382fn find_pairs_brute<const D: usize>(bodies: &[RigidBody<D>]) -> Vec<BroadphasePair> {
383 let mut pairs = Vec::new();
384 for i in 0..bodies.len() {
385 let ti = bodies[i].body_type;
386 for j in (i+1)..bodies.len() {
387 if ti == BodyType::Static && bodies[j].body_type == BodyType::Static { continue; }
388 if !should_collide(&bodies[i], &bodies[j]) { continue; }
389 let (ci, ri) = bodies[i].collider.bounding_sphere();
390 let (cj, rj) = bodies[j].collider.bounding_sphere();
391 let wi = bodies[i].transform.transform_point(&ci);
392 let wj = bodies[j].transform.transform_point(&cj);
393 if wi.distance(&wj) <= ri + rj {
394 pairs.push(BroadphasePair(bodies[i].handle, bodies[j].handle));
395 }
396 }
397 }
398 pairs
399}
400
401#[cfg(test)]
402mod tests {
403 use super::*;
404 use crate::body::RigidBody;
405 use symtropy_math::{Point, Sphere};
406
407 #[test]
408 fn overlapping_pair_detected() {
409 let bodies = vec![
410 RigidBody::<3>::dynamic_sphere(BodyHandle(0), Point::new([0.0, 0.0, 0.0]), 1.0, 1.0),
411 RigidBody::<3>::dynamic_sphere(BodyHandle(1), Point::new([1.5, 0.0, 0.0]), 1.0, 1.0),
412 ];
413 assert_eq!(find_pairs(&bodies).len(), 1);
414 }
415
416 #[test]
417 fn separated_pair_not_detected() {
418 let bodies = vec![
419 RigidBody::<3>::dynamic_sphere(BodyHandle(0), Point::new([0.0, 0.0, 0.0]), 1.0, 1.0),
420 RigidBody::<3>::dynamic_sphere(BodyHandle(1), Point::new([5.0, 0.0, 0.0]), 1.0, 1.0),
421 ];
422 assert!(find_pairs(&bodies).is_empty());
423 }
424
425 #[test]
426 fn static_static_ignored() {
427 let bodies = vec![
428 RigidBody::<3>::static_body(BodyHandle(0), Point::origin(), Box::new(Sphere::unit())),
429 RigidBody::<3>::static_body(BodyHandle(1), Point::new([0.5, 0.0, 0.0]), Box::new(Sphere::unit())),
430 ];
431 assert!(find_pairs(&bodies).is_empty());
432 }
433
434 #[test]
435 fn static_dynamic_detected() {
436 let bodies = vec![
437 RigidBody::<3>::static_body(BodyHandle(0), Point::origin(), Box::new(Sphere::unit())),
438 RigidBody::<3>::dynamic_sphere(BodyHandle(1), Point::new([1.5, 0.0, 0.0]), 1.0, 1.0),
439 ];
440 assert_eq!(find_pairs(&bodies).len(), 1);
441 }
442
443 #[test]
444 fn multiple_pairs() {
445 let bodies = vec![
446 RigidBody::<3>::dynamic_sphere(BodyHandle(0), Point::new([0.0, 0.0, 0.0]), 1.0, 1.0),
447 RigidBody::<3>::dynamic_sphere(BodyHandle(1), Point::new([1.5, 0.0, 0.0]), 1.0, 1.0),
448 RigidBody::<3>::dynamic_sphere(BodyHandle(2), Point::new([10.0, 0.0, 0.0]), 1.0, 1.0),
449 RigidBody::<3>::dynamic_sphere(BodyHandle(3), Point::new([1.0, 1.0, 0.0]), 1.0, 1.0),
450 ];
451 assert!(find_pairs(&bodies).len() >= 2);
452 }
453
454 #[test]
455 fn morton_encode_2d_origin() {
456 let pos = SVector::<f64, 2>::from([0.0, 0.0]);
457 let min = SVector::<f64, 2>::from([0.0, 0.0]);
458 let max = SVector::<f64, 2>::from([100.0, 100.0]);
459 assert_eq!(morton_encode::<2>(&pos, &min, &max), 0);
460 }
461
462 #[test]
463 fn morton_locality() {
464 let min = SVector::<f64, 3>::from([0.0; 3]);
465 let max = SVector::<f64, 3>::from([100.0; 3]);
466 let a = morton_encode::<3>(&SVector::from([10.0, 10.0, 10.0]), &min, &max);
467 let b = morton_encode::<3>(&SVector::from([11.0, 10.0, 10.0]), &min, &max);
468 let far = morton_encode::<3>(&SVector::from([90.0, 90.0, 90.0]), &min, &max);
469 assert!((a as i64 - b as i64).unsigned_abs() < (a as i64 - far as i64).unsigned_abs());
470 }
471
472 #[test]
473 fn lbvh_matches_brute_force() {
474 let mut bodies = Vec::new();
475 for i in 0..(BRUTE_FORCE_THRESHOLD + 10) {
476 let x = (i as f64) * 0.5;
477 bodies.push(RigidBody::<2>::dynamic_sphere(BodyHandle(i), Point::new([x, 0.0]), 1.0, 1.0));
478 }
479 let mut brute = find_pairs_brute(&bodies);
480 brute.sort_by_key(|p| (p.0, p.1));
481 let lbvh = find_pairs(&bodies);
482 for pair in &brute {
483 assert!(lbvh.contains(pair), "LBVH missing pair {:?}", pair);
484 }
485 }
486
487 #[test]
488 fn lbvh_deterministic() {
489 let bodies = vec![
490 RigidBody::<3>::dynamic_sphere(BodyHandle(0), Point::new([0.0, 0.0, 0.0]), 1.0, 1.0),
491 RigidBody::<3>::dynamic_sphere(BodyHandle(1), Point::new([1.5, 0.0, 0.0]), 1.0, 1.0),
492 RigidBody::<3>::dynamic_sphere(BodyHandle(2), Point::new([0.5, 0.5, 0.5]), 0.8, 1.0),
493 ];
494 assert_eq!(find_pairs(&bodies), find_pairs(&bodies));
495 }
496
497 #[test]
498 fn aabb_overlap() {
499 let a = Aabb::<2> { min: SVector::from([0.0, 0.0]), max: SVector::from([2.0, 2.0]) };
500 let b = Aabb::<2> { min: SVector::from([1.0, 1.0]), max: SVector::from([3.0, 3.0]) };
501 let c = Aabb::<2> { min: SVector::from([5.0, 5.0]), max: SVector::from([6.0, 6.0]) };
502 assert!(a.overlaps(&b));
503 assert!(!a.overlaps(&c));
504 }
505
506 #[test]
509 fn incremental_dynamic_static_detected() {
510 let bodies = vec![
511 RigidBody::<3>::static_body(BodyHandle(0), Point::origin(), Box::new(Sphere::unit())),
512 RigidBody::<3>::dynamic_sphere(BodyHandle(1), Point::new([1.5, 0.0, 0.0]), 1.0, 1.0),
513 ];
514 let mut static_bp = super::StaticBroadphase::new();
515 static_bp.rebuild(&bodies);
516
517 let pairs = super::find_pairs_incremental(&bodies, &static_bp);
518 assert_eq!(pairs.len(), 1);
519 let p = pairs[0];
520 assert!((p.0 == BodyHandle(0) && p.1 == BodyHandle(1))
521 || (p.0 == BodyHandle(1) && p.1 == BodyHandle(0)));
522 }
523
524 #[test]
525 fn incremental_static_static_not_produced() {
526 let bodies = vec![
527 RigidBody::<3>::static_body(BodyHandle(0), Point::origin(), Box::new(Sphere::unit())),
528 RigidBody::<3>::static_body(BodyHandle(1), Point::new([0.5, 0.0, 0.0]), Box::new(Sphere::unit())),
529 ];
530 let mut static_bp = super::StaticBroadphase::new();
531 static_bp.rebuild(&bodies);
532
533 let pairs = super::find_pairs_incremental(&bodies, &static_bp);
534 assert!(pairs.is_empty(), "static–static must never collide");
535 }
536
537 #[test]
538 fn incremental_matches_standard_find_pairs() {
539 let bodies = vec![
540 RigidBody::<3>::static_body(BodyHandle(0), Point::new([0.0, 0.0, 0.0]), Box::new(Sphere::unit())),
541 RigidBody::<3>::dynamic_sphere(BodyHandle(1), Point::new([1.5, 0.0, 0.0]), 1.0, 1.0),
542 RigidBody::<3>::dynamic_sphere(BodyHandle(2), Point::new([0.0, 1.5, 0.0]), 1.0, 1.0),
543 RigidBody::<3>::dynamic_sphere(BodyHandle(3), Point::new([10.0, 0.0, 0.0]), 1.0, 1.0),
544 ];
545 let mut static_bp = super::StaticBroadphase::new();
546 static_bp.rebuild(&bodies);
547
548 let mut incremental = super::find_pairs_incremental(&bodies, &static_bp);
549 let mut standard = find_pairs(&bodies);
550 incremental.sort_by_key(|p| (p.0, p.1));
551 standard.sort_by_key(|p| (p.0, p.1));
552 assert_eq!(incremental, standard, "incremental must match standard broadphase");
553 }
554
555 #[test]
556 fn incremental_no_static_degrades_to_standard() {
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 ];
561 let empty_bp = super::StaticBroadphase::<3>::new(); let pairs = super::find_pairs_incremental(&bodies, &empty_bp);
563 assert_eq!(pairs.len(), 1);
564 }
565
566 #[test]
567 fn static_broadphase_rebuild_updates_cache() {
568 let bodies = vec![
569 RigidBody::<3>::static_body(BodyHandle(0), Point::origin(), Box::new(Sphere::unit())),
570 RigidBody::<3>::dynamic_sphere(BodyHandle(1), Point::new([1.5, 0.0, 0.0]), 1.0, 1.0),
571 ];
572 let mut static_bp = super::StaticBroadphase::<3>::new();
573 assert_eq!(static_bp.len(), 0);
574
575 static_bp.rebuild(&bodies);
576 assert_eq!(static_bp.len(), 1, "should cache the one static body");
577 }
578}