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

CratePurpose
collideCore traits: Collider, BoundingVolume, Bounded, Transformable, SpatialPartition
collide-sphereSphere — center + radius. Simplest, most efficient collider
collide-capsuleCapsule — two endpoints + radius. Good for elongated shapes
collide-convexConvex hull — N points + radius. Arbitrary shapes via GJK/EPA
collide-rayRay — origin + direction. Shape crates implement Collider<Ray> via feature flags
collision-detectionCollision 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());
Convex uses heap allocation (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:

MethodComplexityRequired 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

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.