use rayon::prelude::*;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Aabb {
pub min: [f64; 3],
pub max: [f64; 3],
}
impl Aabb {
pub fn new(min: [f64; 3], max: [f64; 3]) -> Self {
Self { min, max }
}
pub fn from_center_half(centre: [f64; 3], half: [f64; 3]) -> Self {
Self {
min: [
centre[0] - half[0],
centre[1] - half[1],
centre[2] - half[2],
],
max: [
centre[0] + half[0],
centre[1] + half[1],
centre[2] + half[2],
],
}
}
pub fn centre(&self) -> [f64; 3] {
[
(self.min[0] + self.max[0]) * 0.5,
(self.min[1] + self.max[1]) * 0.5,
(self.min[2] + self.max[2]) * 0.5,
]
}
pub fn overlaps(&self, other: &Aabb) -> bool {
self.min[0] <= other.max[0]
&& self.max[0] >= other.min[0]
&& self.min[1] <= other.max[1]
&& self.max[1] >= other.min[1]
&& self.min[2] <= other.max[2]
&& self.max[2] >= other.min[2]
}
}
#[derive(Debug, Clone)]
pub struct GpuCollisionBuffer {
pub aabbs: Vec<Aabb>,
pub potential_pairs: Vec<(usize, usize)>,
}
impl GpuCollisionBuffer {
pub fn new() -> Self {
Self {
aabbs: Vec::new(),
potential_pairs: Vec::new(),
}
}
pub fn add_aabb(&mut self, aabb: Aabb) -> usize {
let idx = self.aabbs.len();
self.aabbs.push(aabb);
idx
}
pub fn n_objects(&self) -> usize {
self.aabbs.len()
}
}
impl Default for GpuCollisionBuffer {
fn default() -> Self {
Self::new()
}
}
pub fn gpu_broadphase_sort(buffer: &GpuCollisionBuffer) -> Vec<usize> {
let mut indices: Vec<usize> = (0..buffer.aabbs.len()).collect();
indices.sort_unstable_by(|&a, &b| {
buffer.aabbs[a].min[0]
.partial_cmp(&buffer.aabbs[b].min[0])
.unwrap_or(std::cmp::Ordering::Equal)
});
indices
}
pub fn gpu_aabb_overlap(buffer: &GpuCollisionBuffer) -> Vec<(usize, usize)> {
let n = buffer.aabbs.len();
if n < 2 {
return Vec::new();
}
let pairs: Vec<(usize, usize)> = (0..n)
.into_par_iter()
.flat_map(|i| {
let aabb_i = &buffer.aabbs[i];
(i + 1..n)
.filter(|&j| aabb_i.overlaps(&buffer.aabbs[j]))
.map(|j| (i, j))
.collect::<Vec<_>>()
})
.collect();
pairs
}
#[derive(Debug, Clone, Copy)]
pub struct SphereContact {
pub i: usize,
pub j: usize,
pub depth: f64,
pub normal: [f64; 3],
}
#[derive(Debug, Clone, Copy)]
pub struct Sphere {
pub centre: [f64; 3],
pub radius: f64,
}
impl Sphere {
pub fn new(centre: [f64; 3], radius: f64) -> Self {
Self { centre, radius }
}
}
pub fn gpu_sphere_collision(spheres: &[Sphere], pairs: &[(usize, usize)]) -> Vec<SphereContact> {
pairs
.par_iter()
.filter_map(|&(i, j)| {
if i >= spheres.len() || j >= spheres.len() {
return None;
}
let a = &spheres[i];
let b = &spheres[j];
let dx = b.centre[0] - a.centre[0];
let dy = b.centre[1] - a.centre[1];
let dz = b.centre[2] - a.centre[2];
let dist_sq = dx * dx + dy * dy + dz * dz;
let r_sum = a.radius + b.radius;
if dist_sq >= r_sum * r_sum {
return None;
}
let dist = dist_sq.sqrt().max(1e-15);
let depth = r_sum - dist;
let normal = [dx / dist, dy / dist, dz / dist];
Some(SphereContact {
i,
j,
depth,
normal,
})
})
.collect()
}
pub fn build_collision_pairs(buffer: &mut GpuCollisionBuffer) -> usize {
let sorted = gpu_broadphase_sort(buffer);
let mut pairs = Vec::new();
for (si, &i) in sorted.iter().enumerate() {
let aabb_i = &buffer.aabbs[i];
for &j in sorted[si + 1..].iter() {
let aabb_j = &buffer.aabbs[j];
if aabb_j.min[0] > aabb_i.max[0] {
break;
}
if aabb_i.overlaps(aabb_j) {
let pair = if i < j { (i, j) } else { (j, i) };
pairs.push(pair);
}
}
}
pairs.sort_unstable();
pairs.dedup();
buffer.potential_pairs = pairs;
buffer.potential_pairs.len()
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum GjkResult {
Separated {
distance: f64,
},
Intersecting,
}
pub fn batch_gjk_dispatch(buffer: &GpuCollisionBuffer) -> Vec<GjkResult> {
buffer
.potential_pairs
.par_iter()
.map(|&(i, j)| {
let ci = buffer.aabbs[i].centre();
let cj = buffer.aabbs[j].centre();
let dx = cj[0] - ci[0];
let dy = cj[1] - ci[1];
let dz = cj[2] - ci[2];
let dist = (dx * dx + dy * dy + dz * dz).sqrt();
let diag_i = {
let d = [
buffer.aabbs[i].max[0] - buffer.aabbs[i].min[0],
buffer.aabbs[i].max[1] - buffer.aabbs[i].min[1],
buffer.aabbs[i].max[2] - buffer.aabbs[i].min[2],
];
(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt() * 0.5
};
let diag_j = {
let d = [
buffer.aabbs[j].max[0] - buffer.aabbs[j].min[0],
buffer.aabbs[j].max[1] - buffer.aabbs[j].min[1],
buffer.aabbs[j].max[2] - buffer.aabbs[j].min[2],
];
(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt() * 0.5
};
if dist < diag_i + diag_j {
GjkResult::Intersecting
} else {
GjkResult::Separated {
distance: dist - diag_i - diag_j,
}
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_aabb_new() {
let a = Aabb::new([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
assert_eq!(a.min, [0.0; 3]);
assert_eq!(a.max, [1.0; 3]);
}
#[test]
fn test_aabb_from_center_half() {
let a = Aabb::from_center_half([1.0, 2.0, 3.0], [0.5, 0.5, 0.5]);
assert!((a.min[0] - 0.5).abs() < 1e-12);
assert!((a.max[0] - 1.5).abs() < 1e-12);
assert!((a.min[1] - 1.5).abs() < 1e-12);
assert!((a.max[1] - 2.5).abs() < 1e-12);
}
#[test]
fn test_aabb_centre() {
let a = Aabb::new([0.0, 0.0, 0.0], [2.0, 4.0, 6.0]);
let c = a.centre();
assert!((c[0] - 1.0).abs() < 1e-12);
assert!((c[1] - 2.0).abs() < 1e-12);
assert!((c[2] - 3.0).abs() < 1e-12);
}
#[test]
fn test_aabb_overlaps_true() {
let a = Aabb::new([0.0; 3], [1.0; 3]);
let b = Aabb::new([0.5; 3], [1.5; 3]);
assert!(a.overlaps(&b));
}
#[test]
fn test_aabb_overlaps_false_separated_x() {
let a = Aabb::new([0.0; 3], [1.0; 3]);
let b = Aabb::new([2.0, 0.0, 0.0], [3.0, 1.0, 1.0]);
assert!(!a.overlaps(&b));
}
#[test]
fn test_aabb_overlaps_touching_edge() {
let a = Aabb::new([0.0; 3], [1.0; 3]);
let b = Aabb::new([1.0, 0.0, 0.0], [2.0, 1.0, 1.0]);
assert!(a.overlaps(&b));
}
#[test]
fn test_aabb_no_overlap_y() {
let a = Aabb::new([0.0; 3], [1.0; 3]);
let b = Aabb::new([0.0, 2.0, 0.0], [1.0, 3.0, 1.0]);
assert!(!a.overlaps(&b));
}
#[test]
fn test_buffer_new_empty() {
let buf = GpuCollisionBuffer::new();
assert_eq!(buf.n_objects(), 0);
assert!(buf.potential_pairs.is_empty());
}
#[test]
fn test_buffer_add_aabb() {
let mut buf = GpuCollisionBuffer::new();
let idx = buf.add_aabb(Aabb::new([0.0; 3], [1.0; 3]));
assert_eq!(idx, 0);
assert_eq!(buf.n_objects(), 1);
}
#[test]
fn test_buffer_default() {
let buf = GpuCollisionBuffer::default();
assert_eq!(buf.n_objects(), 0);
}
#[test]
fn test_broadphase_sort_order() {
let mut buf = GpuCollisionBuffer::new();
buf.add_aabb(Aabb::new([3.0, 0.0, 0.0], [4.0, 1.0, 1.0]));
buf.add_aabb(Aabb::new([1.0, 0.0, 0.0], [2.0, 1.0, 1.0]));
buf.add_aabb(Aabb::new([0.0, 0.0, 0.0], [0.5, 1.0, 1.0]));
let perm = gpu_broadphase_sort(&buf);
assert_eq!(perm, vec![2, 1, 0]);
}
#[test]
fn test_broadphase_sort_empty() {
let buf = GpuCollisionBuffer::new();
let perm = gpu_broadphase_sort(&buf);
assert!(perm.is_empty());
}
#[test]
fn test_aabb_overlap_two_overlapping() {
let mut buf = GpuCollisionBuffer::new();
buf.add_aabb(Aabb::new([0.0; 3], [1.0; 3]));
buf.add_aabb(Aabb::new([0.5; 3], [1.5; 3]));
let pairs = gpu_aabb_overlap(&buf);
assert_eq!(pairs.len(), 1);
assert_eq!(pairs[0], (0, 1));
}
#[test]
fn test_aabb_overlap_two_separated() {
let mut buf = GpuCollisionBuffer::new();
buf.add_aabb(Aabb::new([0.0; 3], [1.0; 3]));
buf.add_aabb(Aabb::new([2.0; 3], [3.0; 3]));
let pairs = gpu_aabb_overlap(&buf);
assert!(pairs.is_empty());
}
#[test]
fn test_aabb_overlap_three_objects() {
let mut buf = GpuCollisionBuffer::new();
buf.add_aabb(Aabb::new([0.0; 3], [2.0; 3]));
buf.add_aabb(Aabb::new([1.0; 3], [3.0; 3]));
buf.add_aabb(Aabb::new([5.0; 3], [6.0; 3]));
let pairs = gpu_aabb_overlap(&buf);
assert_eq!(pairs.len(), 1);
assert_eq!(pairs[0], (0, 1));
}
#[test]
fn test_aabb_overlap_single_object_no_pairs() {
let mut buf = GpuCollisionBuffer::new();
buf.add_aabb(Aabb::new([0.0; 3], [1.0; 3]));
let pairs = gpu_aabb_overlap(&buf);
assert!(pairs.is_empty());
}
#[test]
fn test_sphere_collision_overlapping() {
let spheres = vec![
Sphere::new([0.0, 0.0, 0.0], 1.0),
Sphere::new([1.5, 0.0, 0.0], 1.0),
];
let pairs = vec![(0usize, 1usize)];
let contacts = gpu_sphere_collision(&spheres, &pairs);
assert_eq!(contacts.len(), 1);
assert!((contacts[0].depth - 0.5).abs() < 1e-10);
}
#[test]
fn test_sphere_collision_separated() {
let spheres = vec![
Sphere::new([0.0, 0.0, 0.0], 1.0),
Sphere::new([5.0, 0.0, 0.0], 1.0),
];
let pairs = vec![(0usize, 1usize)];
let contacts = gpu_sphere_collision(&spheres, &pairs);
assert!(contacts.is_empty());
}
#[test]
fn test_sphere_collision_normal_direction() {
let spheres = vec![
Sphere::new([0.0, 0.0, 0.0], 1.0),
Sphere::new([1.0, 0.0, 0.0], 1.0),
];
let pairs = vec![(0usize, 1usize)];
let contacts = gpu_sphere_collision(&spheres, &pairs);
assert_eq!(contacts.len(), 1);
assert!((contacts[0].normal[0] - 1.0).abs() < 1e-10);
assert!(contacts[0].normal[1].abs() < 1e-10);
assert!(contacts[0].normal[2].abs() < 1e-10);
}
#[test]
fn test_sphere_collision_out_of_bounds_index() {
let spheres = vec![Sphere::new([0.0; 3], 1.0)];
let pairs = vec![(0usize, 99usize)];
let contacts = gpu_sphere_collision(&spheres, &pairs);
assert!(contacts.is_empty());
}
#[test]
fn test_build_pairs_two_overlapping() {
let mut buf = GpuCollisionBuffer::new();
buf.add_aabb(Aabb::new([0.0; 3], [1.0; 3]));
buf.add_aabb(Aabb::new([0.5; 3], [1.5; 3]));
let n = build_collision_pairs(&mut buf);
assert_eq!(n, 1);
assert_eq!(buf.potential_pairs[0], (0, 1));
}
#[test]
fn test_build_pairs_no_overlap() {
let mut buf = GpuCollisionBuffer::new();
buf.add_aabb(Aabb::new([0.0; 3], [1.0; 3]));
buf.add_aabb(Aabb::new([10.0; 3], [11.0; 3]));
let n = build_collision_pairs(&mut buf);
assert_eq!(n, 0);
}
#[test]
fn test_build_pairs_multiple_objects() {
let mut buf = GpuCollisionBuffer::new();
buf.add_aabb(Aabb::new([0.0; 3], [2.0; 3]));
buf.add_aabb(Aabb::new([1.0; 3], [3.0; 3]));
buf.add_aabb(Aabb::new([1.5; 3], [3.5; 3]));
let n = build_collision_pairs(&mut buf);
assert!(n >= 2); }
#[test]
fn test_gjk_dispatch_intersecting_pair() {
let mut buf = GpuCollisionBuffer::new();
buf.add_aabb(Aabb::new([0.0; 3], [1.0; 3]));
buf.add_aabb(Aabb::new([0.5; 3], [1.5; 3]));
build_collision_pairs(&mut buf);
let results = batch_gjk_dispatch(&buf);
assert!(!results.is_empty());
assert_eq!(results[0], GjkResult::Intersecting);
}
#[test]
fn test_gjk_dispatch_empty_pairs() {
let buf = GpuCollisionBuffer::new();
let results = batch_gjk_dispatch(&buf);
assert!(results.is_empty());
}
#[test]
fn test_gjk_dispatch_separated_pair() {
let mut buf = GpuCollisionBuffer::new();
buf.add_aabb(Aabb::new([0.0; 3], [0.1; 3]));
buf.add_aabb(Aabb::new([10.0; 3], [10.1; 3]));
buf.potential_pairs = vec![(0, 1)];
let results = batch_gjk_dispatch(&buf);
assert_eq!(results.len(), 1);
matches!(results[0], GjkResult::Separated { .. });
}
}