use ga3::Vector;
use crate::{Aabb, bvh::Bvh, triangle};
const BVH_THRESHOLD: usize = 16;
const WALKABLE_NORMAL_Y: f32 = 0.5;
const GROUND_BELOW_TOLERANCE: f32 = 0.2;
const GROUND_REST_TOLERANCE: f32 = 0.05;
const GROUND_OVERLAP_RATIO: f32 = 0.6;
pub struct TriangleMesh {
triangles: Vec<[Vector<f32>; 3]>,
normals: Vec<Vector<f32>>,
bounds: Aabb,
bvh: Option<Bvh>,
}
pub struct TriangleHit {
pub grounded: bool,
pub ground_y: f32,
pub ground_normal: Vector<f32>,
pub push: Vector<f32>,
}
impl TriangleMesh {
pub fn from_vertices(positions: &[[f32; 3]], indices: &[u32]) -> Self {
let mut triangles = Vec::with_capacity(indices.len() / 3);
let mut normals = Vec::with_capacity(indices.len() / 3);
let mut bounds = Aabb::EMPTY;
for chunk in indices.chunks_exact(3) {
let &[index_a, index_b, index_c] = chunk else {
continue;
};
let (Some(&a), Some(&b), Some(&c)) = (
positions.get(index_a as usize),
positions.get(index_b as usize),
positions.get(index_c as usize),
) else {
continue;
};
let corners = [Vector::from(a), Vector::from(b), Vector::from(c)];
let Some(normal) = triangle::face_normal(&corners) else {
continue;
};
for corner in &corners {
bounds.include((*corner).into());
}
triangles.push(corners);
normals.push(normal);
}
let bvh = (triangles.len() > BVH_THRESHOLD).then(|| {
let triangle_bounds: Vec<Aabb> = triangles.iter().map(triangle_bounds).collect();
Bvh::build(&triangle_bounds)
});
Self {
triangles,
normals,
bounds,
bvh,
}
}
pub const fn bounds(&self) -> &Aabb {
&self.bounds
}
pub fn collide_capsule(
&self,
position: Vector<f32>,
velocity_y: f32,
radius: f32,
height: f32,
) -> Option<TriangleHit> {
let capsule_bounds = Aabb {
min: [position.x - radius, position.y, position.z - radius],
max: [
position.x + radius,
position.y + height,
position.z + radius,
],
};
if !self.bounds.overlaps(&capsule_bounds) {
return None;
}
let segment_bottom = position + Vector::y(radius);
let segment_top = position + Vector::y(height - radius);
let feet = position.y;
let falling = velocity_y <= 0.0;
let mut grounded = false;
let mut best_ground_y = f32::NEG_INFINITY;
let mut best_ground_normal = Vector::y(1.0);
let mut push = Vector::new(0.0, 0.0, 0.0);
for index in self.overlapping_triangles(&capsule_bounds) {
let (Some(corners), Some(&normal)) =
(self.triangles.get(index), self.normals.get(index))
else {
continue;
};
if normal.y > WALKABLE_NORMAL_Y {
let Some(ground_y) = triangle::ground_height_at(corners, position.x, position.z)
else {
continue;
};
let overlap = ground_y - feet;
if overlap < -GROUND_BELOW_TOLERANCE || overlap > height * GROUND_OVERLAP_RATIO {
continue;
}
let supported = (falling && overlap > 0.0)
|| (-GROUND_BELOW_TOLERANCE..=GROUND_REST_TOLERANCE).contains(&overlap);
if supported && ground_y > best_ground_y {
grounded = true;
best_ground_y = ground_y;
best_ground_normal = normal;
}
} else if let Some(correction) =
triangle::capsule_push(segment_bottom, segment_top, radius, corners)
{
push += correction;
}
}
if !grounded && push == Vector::new(0.0, 0.0, 0.0) {
return None;
}
Some(TriangleHit {
grounded,
ground_y: best_ground_y,
ground_normal: best_ground_normal,
push,
})
}
pub fn raycast(
&self,
origin: Vector<f32>,
direction: Vector<f32>,
max_distance: f32,
) -> Option<f32> {
let origin_array: [f32; 3] = origin.into();
let inverse_direction = <[f32; 3]>::from(direction).map(f32::recip);
let candidates = self.bvh.as_ref().map_or_else(
|| (0..self.triangles.len()).collect(),
|bvh| bvh.ray_overlapping(origin_array, inverse_direction, max_distance),
);
let mut closest: Option<f32> = None;
for index in candidates {
let Some(corners) = self.triangles.get(index) else {
continue;
};
if let Some(distance) = triangle::ray_intersection(origin, direction, corners)
&& distance <= max_distance
&& closest.is_none_or(|best| distance < best)
{
closest = Some(distance);
}
}
closest
}
fn overlapping_triangles(&self, query: &Aabb) -> Vec<usize> {
self.bvh.as_ref().map_or_else(
|| (0..self.triangles.len()).collect(),
|bvh| bvh.overlapping(query),
)
}
}
fn triangle_bounds(corners: &[Vector<f32>; 3]) -> Aabb {
let mut bounds = Aabb::EMPTY;
for corner in corners {
bounds.include((*corner).into());
}
bounds
}