Collision Detection
A dimension-generic collision detection ecosystem for Rust
Overview
This ecosystem provides collision detection that works in any dimension (2D, 3D, N-D) with any vector type. It consists of a core trait crate, shape implementations, and a collision manager with multiple algorithm tiers.
The central idea: implement the Collider trait for your shapes, put them in a CollisionManager, and pick the algorithm that fits your scene.
Ecosystem
| Crate | Purpose |
|---|---|
collide | Core traits: Collider, BoundingVolume, Bounded, Transformable, SpatialPartition |
collide-sphere | Sphere — center + radius. Simplest, most efficient collider |
collide-capsule | Capsule — two endpoints + radius. Good for elongated shapes |
collide-convex | Convex hull — N points + radius. Arbitrary shapes via GJK/EPA |
collide-ray | Ray — origin + direction. Shape crates implement Collider<Ray> via feature flags |
collision-detection | Collision manager with brute force, spatial partitioning, and BVH |
Getting Started
Add the crates you need:
cargo add collision-detection collide collide-sphere
Create a manager, insert colliders, compute collisions:
use collision_detection::CollisionManager;
use collide_sphere::Sphere;
let mut manager = CollisionManager::<Sphere<Vec3>, u32>::new();
let player = manager.insert_collider(Sphere::new(player_pos, 0.5), PLAYER_ID);
let enemy = manager.insert_collider(Sphere::new(enemy_pos, 0.5), ENEMY_ID);
let collisions = manager.compute_inner_collisions();
for (collider_index, hits) in &collisions {
for hit in hits {
let other_id = hit.index;
let push_vector = hit.info.vector;
}
}
Shapes
All shapes are generic over the vector type V. They work in 2D, 3D, or any dimension as long as V implements InnerSpace.
Sphere
Center + radius. The cheapest collider — collision is a single distance check. Use sphere-only layers when you need maximum throughput.
use collide_sphere::Sphere; let sphere = Sphere::new(center, 1.0); let point = Sphere::point(position); // radius = 0
Sphere also implements BoundingVolume, so it can be used directly as bounding volume for the BVH algorithm.
Capsule
Two endpoints + radius. The convex hull of two spheres. Also represents spheres (start == end), lines (radius == 0), and points.
use collide_capsule::Capsule; let capsule = Capsule::new(0.5, start, end); let sphere = Capsule::sphere(center, 1.0); let line = Capsule::line(start, end);
Convex
N points + radius. The Minkowski sum of a convex hull and a sphere. Uses GJK for distance and EPA for penetration. Handles triangles, boxes, arbitrary polytopes.
use collide_convex::Convex;
let triangle = Convex::new(0.1, vec![p1, p2, p3].into());
let box_shape = Convex::new(0.0, vec![
// 8 corner points of a box
].into());
Box<[V]>) and is Clone but not Copy. For performance-critical paths, prefer Sphere or Capsule when possible.Ray
Origin + direction. Not a collider against itself, but shape crates implement Collider<Ray> via the ray feature flag:
# Cargo.toml
collide-sphere = { version = "...", features = ["ray"] }
collide-capsule = { version = "...", features = ["ray"] }
collide-convex = { version = "...", features = ["ray"] }
use collide::Collider;
use collide_ray::Ray;
use collide_sphere::Sphere;
let ray = Ray::new(origin, direction);
let sphere = Sphere::new(center, 1.0);
if let Some(info) = sphere.collision_info(&ray) {
let hit_distance = info.vector.magnitude();
}
Collision Manager
The CollisionManager<C, I> holds colliders of type C indexed by user IDs of type I. It provides stable indices (via slab), so inserting and removing colliders doesn't invalidate other indices.
let mut manager = CollisionManager::<Sphere<Vec3>, u32>::new(); // Insert returns an internal index (usize) for future reference let idx = manager.insert_collider(sphere, MY_OBJECT_ID); // Update position by replacing the collider manager.replace_collider(idx, new_sphere); // Remove when the object is destroyed manager.remove_collider(idx); // Query methods manager.check_collision(&probe); // bool manager.find_collision(&probe); // Option<I> manager.find_collisions(&probe); // Iterator<Item = I> // Batch computation manager.compute_inner_collisions(); // all pairs within this manager manager.compute_collisions_with(&other); // all pairs with another manager
Layers
There is no built-in layer/mask system. Instead, use separate CollisionManager instances as layers:
// Layer 1: static world geometry let mut static_layer = CollisionManager::<Sphere<Vec3>, u32>::new(); static_layer.insert_collider(wall, WALL_ID); static_layer.insert_collider(floor, FLOOR_ID); // Layer 2: dynamic objects let mut dynamic_layer = CollisionManager::<Sphere<Vec3>, u32>::new(); dynamic_layer.insert_collider(player, PLAYER_ID); dynamic_layer.insert_collider(enemy, ENEMY_ID); // Dynamic objects collide with each other let mutual = dynamic_layer.compute_inner_collisions(); // Dynamic objects collide with static world // (no compute_inner_collisions on static — they never move) let world = dynamic_layer.compute_collisions_with(&static_layer);
This naturally gives you the "static vs dynamic" optimization. Each layer uses one collider type for maximum efficiency. If you need mixed shapes in one layer, use an enum:
enum Shape {
Sphere(Sphere<Vec3>),
Capsule(Capsule<Vec3>),
}
impl Collider for Shape {
type Vector = Vec3;
fn collision_info(&self, other: &Self) -> Option<CollisionInfo<Vec3>> {
// dispatch based on variant combination
}
}
Algorithms
Every manager supports three algorithm variants with the same return type. Start with brute force, add trait implementations to unlock faster algorithms as your scene grows:
| Method | Complexity | Required Traits |
|---|---|---|
compute_inner_collisions() | O(n²) | Collider |
compute_inner_collisions_spatial() | O(n×k) | Collider + SpatialPartition |
compute_inner_collisions_bvh::<V>() | O(n log n) | Collider + Bounded<V> |
Same three variants exist for compute_collisions_with.
Brute Force
Tests all pairs. No additional traits needed. Fine for small scenes (<100 objects).
Spatial Partitioning
Implement SpatialPartition to map your collider to grid cells. The manager builds a HashMap<Cell, Vec<index>> and only checks pairs sharing a cell. Best for uniformly distributed objects of similar size.
impl SpatialPartition for MyCollider {
type Cell = [i32; 3];
fn cells(&self) -> impl Iterator<Item = [i32; 3]> {
// return all grid cells this collider overlaps
}
}
BVH (Bounding Volume Hierarchy)
Implement Bounded<V> to provide a bounding volume. The type parameter V selects which bounding volume to use — Sphere already implements BoundingVolume, so it works out of the box as bounding volume.
use collide::Bounded;
use collide_sphere::Sphere;
impl Bounded<Sphere<Vec3>> for MyCollider {
fn bounding_volume(&self) -> Sphere<Vec3> {
Sphere::new(self.center(), self.max_radius())
}
}
// Then use:
manager.compute_inner_collisions_bvh::<Sphere<Vec3>>();
Bounded<B> is generic, so you can provide multiple bounding volume types and pick the best one per scene.
Composable Wrappers
BoundedCollider<B, C>
Wraps a collider with a bounding volume for automatic pre-checks. The bounding volume's check_collision runs first — only if it passes, the inner collider is tested. Nestable for cascading broad-to-narrow:
use collide::BoundedCollider; // Cheap sphere check before expensive convex GJK type FastConvex = BoundedCollider<Sphere<Vec3>, Convex<Vec3>>; let collider = FastConvex::new(my_convex); // sphere computed automatically
Transformed<C, T>
Wraps a collider with a transform. The shape must implement Transformable<T>, and the transform must implement Transform<V> (from vector-space).
use collide::{Transformed, Transformable};
let world_collider = Transformed::new(local_shape, my_transform);
Both shapes are materialized in world space before collision testing. For Copy types (Sphere, Capsule) this is free. For Convex it involves heap allocation.
Writing a Custom Collider
Implement the Collider trait:
use collide::{Collider, CollisionInfo};
struct MyShape<V> { /* your fields */ }
impl<V> Collider for MyShape<V> {
type Vector = V;
fn check_collision(&self, other: &Self) -> bool {
// Optional: cheap broad-phase test
// Default calls collision_info().is_some()
}
fn collision_info(&self, other: &Self) -> Option<CollisionInfo<V>> {
// Return contact points and separation vector,
// or None if no collision
}
}
All algorithms call check_collision before collision_info. Override check_collision with a cheap test (bounding sphere distance) to skip expensive narrow-phase math for non-colliding pairs.
Design Principles
- Dimension-generic: All traits work in 2D, 3D, N-D. No dimension-specific assumptions anywhere.
- No math library lock-in: Works with any vector type that implements
VectorSpace/InnerSpace. - Composable: Wrappers (
BoundedCollider,Transformed) compose freely with any collider. - Progressive complexity: Start with brute force, add trait impls to unlock faster algorithms without changing existing code.
This project is designed for use with AI coding assistants. Consistent API across all algorithm variants (same return type, same collision info structure), composable wrapper types, and uniform naming conventions.