Skip to main content

collide_mesh/
mesh.rs

1use ga3::Vector;
2use inner_space::DotProduct;
3
4use crate::{Aabb, Ground, Motion, bvh::Bvh, triangle};
5
6const BVH_THRESHOLD: usize = 16;
7const WALKABLE_NORMAL_THRESHOLD: f32 = 0.5;
8const GROUND_BELOW_TOLERANCE: f32 = 0.2;
9const GROUND_REST_TOLERANCE: f32 = 0.05;
10const GROUND_OVERLAP_RATIO: f32 = 0.6;
11
12/// A static triangle mesh prepared for capsule collision and raycasts.
13pub struct TriangleMesh {
14    triangles: Vec<[Vector<f32>; 3]>,
15    normals: Vec<Vector<f32>>,
16    bounds: Aabb,
17    bvh: Option<Bvh>,
18}
19
20/// The result of resolving a capsule against a single mesh.
21pub struct TriangleHit {
22    /// The walkable ground of this mesh supporting the capsule, if any.
23    pub ground: Option<Ground>,
24    /// The accumulated displacement out of steep geometry.
25    pub push: Vector<f32>,
26}
27
28impl TriangleMesh {
29    /// Builds a mesh from raw vertex positions and triangle indices.
30    ///
31    /// Degenerate triangles and out-of-range indices are skipped.
32    /// A bounding volume hierarchy is built once the mesh exceeds 16 triangles.
33    #[must_use]
34    pub fn from_vertices(positions: &[[f32; 3]], indices: &[u32]) -> Self {
35        let mut triangles = Vec::with_capacity(indices.len() / 3);
36        let mut normals = Vec::with_capacity(indices.len() / 3);
37        let mut bounds = Aabb::EMPTY;
38
39        for chunk in indices.chunks_exact(3) {
40            let &[index_a, index_b, index_c] = chunk else {
41                continue;
42            };
43            let (Some(&a), Some(&b), Some(&c)) = (
44                positions.get(index_a as usize),
45                positions.get(index_b as usize),
46                positions.get(index_c as usize),
47            ) else {
48                continue;
49            };
50
51            let corners = [Vector::from(a), Vector::from(b), Vector::from(c)];
52            let Some(normal) = triangle::face_normal(&corners) else {
53                continue;
54            };
55
56            for corner in &corners {
57                bounds.include((*corner).into());
58            }
59            triangles.push(corners);
60            normals.push(normal);
61        }
62
63        let bvh = (triangles.len() > BVH_THRESHOLD).then(|| {
64            let triangle_bounds: Vec<Aabb> = triangles.iter().map(triangle_bounds).collect();
65            Bvh::build(&triangle_bounds)
66        });
67
68        Self {
69            triangles,
70            normals,
71            bounds,
72            bvh,
73        }
74    }
75
76    /// Returns the bounding box of the whole mesh.
77    #[must_use]
78    pub const fn bounds(&self) -> &Aabb {
79        &self.bounds
80    }
81
82    /// Resolves a capsule aligned with `up` against this mesh.
83    ///
84    /// `segment_bottom`/`segment_top` are the capsule's inner segment
85    /// endpoints (sphere centers), `up` the unit vector walkable ground is
86    /// measured against. Returns `None` when nothing touches.
87    #[must_use]
88    pub fn collide_capsule(
89        &self,
90        segment_bottom: Vector<f32>,
91        segment_top: Vector<f32>,
92        radius: f32,
93        up: Vector<f32>,
94        motion: Motion,
95    ) -> Option<TriangleHit> {
96        let pad = Vector::new(radius, radius, radius);
97        let mut capsule_bounds = Aabb::EMPTY;
98        capsule_bounds.include((segment_bottom - pad).into());
99        capsule_bounds.include((segment_bottom + pad).into());
100        capsule_bounds.include((segment_top - pad).into());
101        capsule_bounds.include((segment_top + pad).into());
102        if !self.bounds.overlaps(&capsule_bounds) {
103            return None;
104        }
105
106        let feet = segment_bottom - up * radius;
107        let height = (segment_top - segment_bottom).dot(&up) + 2.0 * radius;
108
109        let mut ground: Option<Ground> = None;
110        let mut push = Vector::new(0.0, 0.0, 0.0);
111
112        for index in self.overlapping_triangles(&capsule_bounds) {
113            let (Some(corners), Some(&normal)) =
114                (self.triangles.get(index), self.normals.get(index))
115            else {
116                continue;
117            };
118
119            if normal.dot(&up) > WALKABLE_NORMAL_THRESHOLD {
120                let Some(distance) = triangle::ground_distance_at(corners, feet, up) else {
121                    continue;
122                };
123                if distance < -GROUND_BELOW_TOLERANCE || distance > height * GROUND_OVERLAP_RATIO {
124                    continue;
125                }
126                let supported = (motion == Motion::Falling && distance > 0.0)
127                    || (-GROUND_BELOW_TOLERANCE..=GROUND_REST_TOLERANCE).contains(&distance);
128                if supported && ground.is_none_or(|best| distance > best.distance) {
129                    ground = Some(Ground { distance, normal });
130                }
131            } else if let Some(correction) =
132                triangle::capsule_push(segment_bottom, segment_top, radius, corners)
133            {
134                push += correction;
135            }
136        }
137
138        if ground.is_none() && push == Vector::new(0.0, 0.0, 0.0) {
139            return None;
140        }
141
142        Some(TriangleHit { ground, push })
143    }
144
145    /// Returns the distance to the closest triangle hit by the ray, if any.
146    pub fn raycast(
147        &self,
148        origin: Vector<f32>,
149        direction: Vector<f32>,
150        max_distance: f32,
151    ) -> Option<f32> {
152        let origin_array: [f32; 3] = origin.into();
153        let inverse_direction = <[f32; 3]>::from(direction).map(f32::recip);
154
155        let candidates = self.bvh.as_ref().map_or_else(
156            || (0..self.triangles.len()).collect(),
157            |bvh| bvh.ray_overlapping(origin_array, inverse_direction, max_distance),
158        );
159
160        let mut closest: Option<f32> = None;
161        for index in candidates {
162            let Some(corners) = self.triangles.get(index) else {
163                continue;
164            };
165            if let Some(distance) = triangle::ray_intersection(origin, direction, corners)
166                && distance <= max_distance
167                && closest.is_none_or(|best| distance < best)
168            {
169                closest = Some(distance);
170            }
171        }
172        closest
173    }
174
175    fn overlapping_triangles(&self, query: &Aabb) -> Vec<usize> {
176        self.bvh.as_ref().map_or_else(
177            || (0..self.triangles.len()).collect(),
178            |bvh| bvh.overlapping(query),
179        )
180    }
181}
182
183fn triangle_bounds(corners: &[Vector<f32>; 3]) -> Aabb {
184    let mut bounds = Aabb::EMPTY;
185    for corner in corners {
186        bounds.include((*corner).into());
187    }
188    bounds
189}