Skip to main content

collide_mesh/
lib.rs

1#![deny(missing_docs)]
2
3//! Triangle mesh collision for character controllers.
4//!
5//! This crate is deliberately 3D-only and uses [`ga3::Vector<f32>`] directly:
6//! triangle meshes have no meaningful N-dimensional generalization, and the
7//! ground semantics (walkable slopes, ground height) are inherently
8//! three-dimensional gameplay concepts. The up direction is a caller-supplied
9//! unit vector, not fixed to an axis: [`CollisionWorld::collide_capsule`]'s
10//! ground classification, height bookkeeping and capsule alignment all follow
11//! it, so a world that isn't Y-up needs no conversion at this boundary.
12
13mod bvh;
14mod mesh;
15mod triangle;
16
17pub use mesh::{TriangleHit, TriangleMesh};
18
19use collide_capsule::Capsule;
20use collide_ray::Ray;
21use ga3::Vector;
22use inner_space::DotProduct;
23
24use bvh::Bvh;
25
26/// An axis-aligned bounding box in 3D space.
27#[derive(Clone, Copy)]
28pub struct Aabb {
29    /// The corner with the smallest coordinates on all axes.
30    pub min: [f32; 3],
31    /// The corner with the largest coordinates on all axes.
32    pub max: [f32; 3],
33}
34
35impl Aabb {
36    /// The empty box: including the first point turns it into that point.
37    pub const EMPTY: Self = Self {
38        min: [f32::INFINITY; 3],
39        max: [f32::NEG_INFINITY; 3],
40    };
41
42    /// Checks if two boxes overlap, boundaries included.
43    #[must_use]
44    pub fn overlaps(&self, other: &Self) -> bool {
45        (0..3).all(|axis| self.max[axis] >= other.min[axis] && self.min[axis] <= other.max[axis])
46    }
47
48    /// Grows the box to contain the given point.
49    pub fn include(&mut self, point: [f32; 3]) {
50        for ((minimum, maximum), value) in self.min.iter_mut().zip(&mut self.max).zip(point) {
51            *minimum = minimum.min(value);
52            *maximum = maximum.max(value);
53        }
54    }
55
56    /// Returns the smallest box containing both boxes.
57    #[must_use]
58    pub fn merged(&self, other: &Self) -> Self {
59        let mut result = *self;
60        result.include(other.min);
61        result.include(other.max);
62        result
63    }
64}
65
66/// Walkable ground supporting a capsule.
67#[derive(Clone, Copy)]
68pub struct Ground {
69    /// Signed distance along `up` from the capsule's feet to the ground:
70    /// positive means the feet are below it (move the capsule along `up` by
71    /// this much to rest on it), negative means they hover above it.
72    pub distance: f32,
73    /// The face normal of the ground.
74    pub normal: Vector<f32>,
75}
76
77/// Whether a capsule is moving along `up` or against it.
78#[derive(Clone, Copy, PartialEq, Eq)]
79pub enum Motion {
80    /// Moving against `up` or resting: ground below can support the capsule.
81    Falling,
82    /// Moving along `up`: ground is passed rather than landed on.
83    Rising,
84}
85
86impl Motion {
87    /// Classifies a velocity against the up direction.
88    #[must_use]
89    pub fn from_velocity(velocity: Vector<f32>, up: Vector<f32>) -> Self {
90        if velocity.dot(&up) <= 0.0 {
91            Self::Falling
92        } else {
93            Self::Rising
94        }
95    }
96}
97
98/// The combined result of resolving a capsule against a [`CollisionWorld`].
99pub struct CollisionResult {
100    /// The walkable ground supporting the capsule, if any.
101    pub ground: Option<Ground>,
102    /// The accumulated displacement pushing the capsule out of steep geometry.
103    pub push: Vector<f32>,
104}
105
106/// A set of triangle meshes behind a bounding volume hierarchy.
107pub struct CollisionWorld {
108    meshes: Vec<TriangleMesh>,
109    bvh: Bvh,
110}
111
112impl CollisionWorld {
113    /// Creates a world from a set of meshes.
114    #[must_use]
115    pub fn new(meshes: Vec<TriangleMesh>) -> Self {
116        let bounds: Vec<Aabb> = meshes.iter().map(|mesh| *mesh.bounds()).collect();
117        Self {
118            bvh: Bvh::build(&bounds),
119            meshes,
120        }
121    }
122
123    /// Resolves a capsule aligned with `up` against all meshes.
124    ///
125    /// `up` is the unit vector walkable ground is measured against; `velocity`
126    /// distinguishes landing on ground (falling or resting) from passing it
127    /// while moving up.
128    #[must_use]
129    pub fn collide_capsule(
130        &self,
131        capsule: &Capsule<Vector<f32>>,
132        up: Vector<f32>,
133        velocity: Vector<f32>,
134    ) -> CollisionResult {
135        let radius = capsule.rad;
136        let motion = Motion::from_velocity(velocity, up);
137
138        let mut result = CollisionResult {
139            ground: None,
140            push: Vector::new(0.0, 0.0, 0.0),
141        };
142
143        let pad = Vector::new(radius, radius, radius);
144        let mut capsule_bounds = Aabb::EMPTY;
145        capsule_bounds.include((capsule.start - pad).into());
146        capsule_bounds.include((capsule.start + pad).into());
147        capsule_bounds.include((capsule.end - pad).into());
148        capsule_bounds.include((capsule.end + pad).into());
149
150        for mesh_index in self.bvh.overlapping(&capsule_bounds) {
151            let Some(mesh) = self.meshes.get(mesh_index) else {
152                continue;
153            };
154            let Some(hit) = mesh.collide_capsule(capsule.start, capsule.end, radius, up, motion)
155            else {
156                continue;
157            };
158
159            if let Some(ground) = hit.ground
160                && result
161                    .ground
162                    .is_none_or(|best| ground.distance > best.distance)
163            {
164                result.ground = Some(ground);
165            }
166            result.push += hit.push;
167        }
168
169        result
170    }
171
172    /// Returns the distance to the closest mesh hit by the ray, if any.
173    ///
174    /// The ray direction is expected to be normalized; the result is limited
175    /// to `max_distance`.
176    pub fn raycast(&self, ray: &Ray<Vector<f32>>, max_distance: f32) -> Option<f32> {
177        let origin: [f32; 3] = ray.origin.into();
178        let inverse_direction = <[f32; 3]>::from(ray.direction).map(f32::recip);
179
180        let mut closest: Option<f32> = None;
181        for mesh_index in self
182            .bvh
183            .ray_overlapping(origin, inverse_direction, max_distance)
184        {
185            let Some(mesh) = self.meshes.get(mesh_index) else {
186                continue;
187            };
188            if let Some(distance) = mesh.raycast(ray.origin, ray.direction, max_distance)
189                && closest.is_none_or(|best| distance < best)
190            {
191                closest = Some(distance);
192            }
193        }
194        closest
195    }
196}