Skip to main content

collide_mesh/
mesh.rs

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