#![allow(dead_code)]
use crate::mesh::MeshBuffers;
use crate::normals::compute_normals;
fn sub(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
fn add(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
fn cross(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
}
fn dot(a: [f32; 3], b: [f32; 3]) -> f32 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
fn length(v: [f32; 3]) -> f32 {
(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt()
}
fn normalize(v: [f32; 3]) -> Option<[f32; 3]> {
let len = length(v);
if len < 1e-10 {
None
} else {
Some([v[0] / len, v[1] / len, v[2] / len])
}
}
fn scale(v: [f32; 3], s: f32) -> [f32; 3] {
[v[0] * s, v[1] * s, v[2] * s]
}
#[derive(Clone, Debug)]
struct HullFace {
verts: [usize; 3],
normal: [f32; 3],
center: [f32; 3],
}
impl HullFace {
fn new(pts: &[[f32; 3]], a: usize, b: usize, c: usize) -> Option<Self> {
let pa = pts[a];
let pb = pts[b];
let pc = pts[c];
let e1 = sub(pb, pa);
let e2 = sub(pc, pa);
let n = cross(e1, e2);
let normal = normalize(n)?;
let center = [
(pa[0] + pb[0] + pc[0]) / 3.0,
(pa[1] + pb[1] + pc[1]) / 3.0,
(pa[2] + pb[2] + pc[2]) / 3.0,
];
Some(HullFace {
verts: [a, b, c],
normal,
center,
})
}
fn signed_distance(&self, p: [f32; 3]) -> f32 {
dot(self.normal, sub(p, self.center))
}
fn is_visible_from(&self, p: [f32; 3]) -> bool {
self.signed_distance(p) > 1e-7
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct Edge {
from: usize,
to: usize,
}
impl Edge {
fn reversed(self) -> Self {
Edge {
from: self.to,
to: self.from,
}
}
}
fn horizon_edges(faces: &[HullFace], visible: &[bool]) -> Vec<Edge> {
let mut visible_edges: Vec<Edge> = Vec::new();
for (fi, face) in faces.iter().enumerate() {
if !visible[fi] {
continue;
}
let [a, b, c] = face.verts;
visible_edges.push(Edge { from: a, to: b });
visible_edges.push(Edge { from: b, to: c });
visible_edges.push(Edge { from: c, to: a });
}
visible_edges
.iter()
.filter(|e| !visible_edges.contains(&e.reversed()))
.copied()
.collect()
}
fn find_initial_tetrahedron(pts: &[[f32; 3]]) -> Option<(usize, usize, usize, usize)> {
let n = pts.len();
if n < 4 {
return None;
}
let i0 = pts
.iter()
.enumerate()
.min_by(|a, b| {
a.1[1]
.partial_cmp(&b.1[1])
.unwrap_or(std::cmp::Ordering::Equal)
.then(
a.1[0]
.partial_cmp(&b.1[0])
.unwrap_or(std::cmp::Ordering::Equal),
)
})
.map(|(i, _)| i)?;
let i1 = pts
.iter()
.enumerate()
.filter(|(i, _)| *i != i0)
.max_by(|a, b| {
let da = length(sub(*a.1, pts[i0]));
let db = length(sub(*b.1, pts[i0]));
da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
})
.map(|(i, _)| i)?;
let axis = sub(pts[i1], pts[i0]);
let i2 = pts
.iter()
.enumerate()
.filter(|(i, _)| *i != i0 && *i != i1)
.max_by(|a, b| {
let da = length(cross(sub(*a.1, pts[i0]), axis));
let db = length(cross(sub(*b.1, pts[i0]), axis));
da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
})
.map(|(i, _)| i)?;
let axis2 = sub(pts[i1], pts[i0]);
let cross2 = cross(sub(pts[i2], pts[i0]), axis2);
if length(cross2) < 1e-7 {
return None; }
let plane_n = cross(sub(pts[i1], pts[i0]), sub(pts[i2], pts[i0]));
let plane_n_len = length(plane_n);
if plane_n_len < 1e-10 {
return None;
}
let plane_n_unit = scale(plane_n, 1.0 / plane_n_len);
let i3 = pts
.iter()
.enumerate()
.filter(|(i, _)| *i != i0 && *i != i1 && *i != i2)
.max_by(|a, b| {
let da = dot(plane_n_unit, sub(*a.1, pts[i0])).abs();
let db = dot(plane_n_unit, sub(*b.1, pts[i0])).abs();
da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
})
.map(|(i, _)| i)?;
let dist = dot(plane_n_unit, sub(pts[i3], pts[i0])).abs();
if dist < 1e-7 {
return None; }
Some((i0, i1, i2, i3))
}
fn build_tetrahedron(
pts: &[[f32; 3]],
i0: usize,
i1: usize,
i2: usize,
i3: usize,
) -> Option<Vec<HullFace>> {
let centroid = [
(pts[i0][0] + pts[i1][0] + pts[i2][0] + pts[i3][0]) / 4.0,
(pts[i0][1] + pts[i1][1] + pts[i2][1] + pts[i3][1]) / 4.0,
(pts[i0][2] + pts[i1][2] + pts[i2][2] + pts[i3][2]) / 4.0,
];
let mut faces = Vec::new();
let face_verts = [[i0, i1, i2], [i0, i1, i3], [i0, i2, i3], [i1, i2, i3]];
for [a, b, c] in face_verts {
let mut face = HullFace::new(pts, a, b, c)?;
if dot(face.normal, sub(centroid, face.center)) > 0.0 {
face.normal = scale(face.normal, -1.0);
face.verts = [face.verts[0], face.verts[2], face.verts[1]];
}
faces.push(face);
}
Some(faces)
}
pub struct ConvexHull {
pub vertices: Vec<[f32; 3]>,
pub indices: Vec<u32>,
pub vertex_indices: Vec<usize>,
}
impl ConvexHull {
pub fn to_mesh_buffers(&self) -> MeshBuffers {
let n = self.vertices.len();
let mut m = MeshBuffers {
positions: self.vertices.clone(),
normals: vec![[0.0, 1.0, 0.0]; n],
tangents: vec![[1.0, 0.0, 0.0, 1.0]; n],
uvs: vec![[0.0, 0.0]; n],
indices: self.indices.clone(),
colors: None,
has_suit: false,
};
compute_normals(&mut m);
m
}
pub fn face_count(&self) -> usize {
self.indices.len() / 3
}
pub fn volume(&self) -> f32 {
let mut vol = 0.0f32;
for tri in self.indices.chunks_exact(3) {
let a = self.vertices[tri[0] as usize];
let b = self.vertices[tri[1] as usize];
let c = self.vertices[tri[2] as usize];
vol += (a[0] * (b[1] * c[2] - b[2] * c[1])
+ a[1] * (b[2] * c[0] - b[0] * c[2])
+ a[2] * (b[0] * c[1] - b[1] * c[0]))
/ 6.0;
}
vol.abs()
}
pub fn surface_area(&self) -> f32 {
let mut area = 0.0f32;
for tri in self.indices.chunks_exact(3) {
let a = self.vertices[tri[0] as usize];
let b = self.vertices[tri[1] as usize];
let c = self.vertices[tri[2] as usize];
let e1 = sub(b, a);
let e2 = sub(c, a);
area += length(cross(e1, e2)) * 0.5;
}
area
}
}
pub fn convex_hull(points: &[[f32; 3]]) -> Option<ConvexHull> {
if points.len() < 4 {
return None;
}
let (i0, i1, i2, i3) = find_initial_tetrahedron(points)?;
let mut faces = build_tetrahedron(points, i0, i1, i2, i3)?;
for (pi, &p) in points.iter().enumerate() {
if pi == i0 || pi == i1 || pi == i2 || pi == i3 {
continue;
}
let visible: Vec<bool> = faces.iter().map(|f| f.is_visible_from(p)).collect();
if !visible.iter().any(|&v| v) {
continue;
}
let horizon = horizon_edges(&faces, &visible);
if horizon.is_empty() {
continue;
}
let mut new_faces: Vec<HullFace> = faces
.into_iter()
.zip(visible.iter())
.filter_map(|(f, &vis)| if vis { None } else { Some(f) })
.collect();
let interior = [
(points[i0][0] + points[i1][0] + points[i2][0] + points[i3][0]) / 4.0,
(points[i0][1] + points[i1][1] + points[i2][1] + points[i3][1]) / 4.0,
(points[i0][2] + points[i1][2] + points[i2][2] + points[i3][2]) / 4.0,
];
for edge in horizon {
if let Some(mut face) = HullFace::new(points, edge.from, edge.to, pi) {
if dot(face.normal, sub(interior, face.center)) > 0.0 {
face.normal = scale(face.normal, -1.0);
face.verts = [face.verts[0], face.verts[2], face.verts[1]];
}
new_faces.push(face);
}
}
faces = new_faces;
}
if faces.is_empty() {
return None;
}
let mut hull_vert_set: Vec<usize> = faces.iter().flat_map(|f| f.verts).collect();
hull_vert_set.sort_unstable();
hull_vert_set.dedup();
let vertex_indices = hull_vert_set.clone();
let vertices: Vec<[f32; 3]> = vertex_indices.iter().map(|&i| points[i]).collect();
let orig_to_hull: std::collections::HashMap<usize, u32> = vertex_indices
.iter()
.enumerate()
.map(|(hi, &oi)| (oi, hi as u32))
.collect();
let indices: Vec<u32> = faces
.iter()
.flat_map(|f| {
[
orig_to_hull[&f.verts[0]],
orig_to_hull[&f.verts[1]],
orig_to_hull[&f.verts[2]],
]
})
.collect();
Some(ConvexHull {
vertices,
indices,
vertex_indices,
})
}
pub fn mesh_convex_hull(mesh: &MeshBuffers) -> Option<ConvexHull> {
convex_hull(&mesh.positions)
}
pub fn point_in_hull(hull: &ConvexHull, point: [f32; 3]) -> bool {
const EPSILON: f32 = 1e-4;
for tri in hull.indices.chunks_exact(3) {
let a = hull.vertices[tri[0] as usize];
let b = hull.vertices[tri[1] as usize];
let c = hull.vertices[tri[2] as usize];
let e1 = sub(b, a);
let e2 = sub(c, a);
let n_raw = cross(e1, e2);
if let Some(n) = normalize(n_raw) {
let d = dot(n, sub(point, a));
if d > EPSILON {
return false;
}
}
}
true
}
#[cfg(test)]
mod tests {
use super::*;
use crate::shapes::sphere;
fn cube_pts() -> Vec<[f32; 3]> {
vec![
[-1., -1., -1.],
[1., -1., -1.],
[1., 1., -1.],
[-1., 1., -1.],
[-1., -1., 1.],
[1., -1., 1.],
[1., 1., 1.],
[-1., 1., 1.],
]
}
fn tetra_pts() -> Vec<[f32; 3]> {
vec![
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0],
]
}
#[test]
fn convex_hull_of_cube_vertices() {
let pts = cube_pts();
let hull = convex_hull(&pts).expect("cube hull should succeed");
assert_eq!(
hull.face_count(),
12,
"cube hull should have 12 triangular faces"
);
}
#[test]
fn convex_hull_vertex_count_lte_input() {
let pts = cube_pts();
let hull = convex_hull(&pts).expect("hull should succeed");
assert!(
hull.vertices.len() <= pts.len(),
"hull vertices ({}) must not exceed input ({})",
hull.vertices.len(),
pts.len()
);
}
#[test]
fn convex_hull_all_input_inside_or_on_hull() {
let pts = cube_pts();
let hull = convex_hull(&pts).expect("hull should succeed");
for &p in &pts {
assert!(
point_in_hull(&hull, p),
"input point {:?} should be inside or on hull",
p
);
}
}
#[test]
fn convex_hull_tetrahedron_has_4_faces() {
let pts = tetra_pts();
let hull = convex_hull(&pts).expect("tetrahedron hull should succeed");
assert_eq!(
hull.face_count(),
4,
"tetrahedron hull must have exactly 4 faces"
);
}
#[test]
fn convex_hull_returns_none_for_fewer_than_4_points() {
assert!(convex_hull(&[[0.0, 0.0, 0.0]]).is_none());
assert!(convex_hull(&[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]).is_none());
}
#[test]
fn convex_hull_to_mesh_buffers_has_valid_indices() {
let pts = tetra_pts();
let hull = convex_hull(&pts).expect("hull should succeed");
let mesh = hull.to_mesh_buffers();
let n = mesh.positions.len() as u32;
for &idx in &mesh.indices {
assert!(idx < n, "index {} out of bounds (n={})", idx, n);
}
}
#[test]
fn convex_hull_face_count_positive() {
let pts = cube_pts();
let hull = convex_hull(&pts).expect("hull should succeed");
assert!(hull.face_count() > 0, "hull must have at least one face");
}
#[test]
fn convex_hull_volume_positive() {
let pts = cube_pts();
let hull = convex_hull(&pts).expect("hull should succeed");
let vol = hull.volume();
assert!(vol > 0.0, "hull volume must be positive, got {}", vol);
assert!(
(vol - 8.0).abs() < 0.1,
"cube hull volume should be ~8, got {}",
vol
);
}
#[test]
fn convex_hull_surface_area_positive() {
let pts = cube_pts();
let hull = convex_hull(&pts).expect("hull should succeed");
let area = hull.surface_area();
assert!(
area > 0.0,
"hull surface area must be positive, got {}",
area
);
assert!(
(area - 24.0).abs() < 0.1,
"cube hull surface area should be ~24, got {}",
area
);
}
#[test]
fn point_in_hull_center_is_inside() {
let pts = cube_pts();
let hull = convex_hull(&pts).expect("hull should succeed");
assert!(
point_in_hull(&hull, [0.0, 0.0, 0.0]),
"center of cube hull should be inside"
);
}
#[test]
fn point_in_hull_far_point_is_outside() {
let pts = cube_pts();
let hull = convex_hull(&pts).expect("hull should succeed");
assert!(
!point_in_hull(&hull, [10.0, 10.0, 10.0]),
"far-away point should be outside hull"
);
}
#[test]
fn mesh_convex_hull_works_on_sphere_mesh() {
let s = sphere(1.0, 6, 6);
let hull = mesh_convex_hull(&s).expect("sphere mesh hull should succeed");
assert!(hull.face_count() > 0, "sphere hull must have faces");
assert!(
point_in_hull(&hull, [0.0, 0.0, 0.0]),
"sphere center should be inside hull"
);
assert!(
hull.vertices.len() <= s.positions.len(),
"hull vertices ({}) must not exceed sphere vertex count ({})",
hull.vertices.len(),
s.positions.len()
);
assert!(
!point_in_hull(&hull, [0.0, 5.0, 0.0]),
"far point should be outside sphere hull"
);
}
#[test]
fn convex_hull_coplanar_returns_none() {
let pts = vec![
[0.0f32, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[1.0, 1.0, 0.0],
[0.5, 0.5, 0.0],
];
assert!(
convex_hull(&pts).is_none(),
"coplanar points should return None"
);
}
}