Skip to main content

gizmo_physics_core/
quickhull.rs

1use gizmo_math::Vec3;
2use std::collections::{HashMap, HashSet};
3
4const EPSILON: f32 = 1e-4;
5
6#[derive(Clone, Debug)]
7pub struct HullFace {
8    pub v: [usize; 3],
9    pub normal: Vec3,
10    pub outside_set: Vec<usize>,
11}
12
13impl HullFace {
14    pub fn new(a: usize, b: usize, c: usize, points: &[Vec3]) -> Self {
15        let pa = points[a];
16        let pb = points[b];
17        let pc = points[c];
18
19        let mut normal = (pb - pa).cross(pc - pa);
20        let len = normal.length();
21        if len > 1e-8 {
22            normal /= len;
23        } else {
24            normal = Vec3::Y;
25        }
26
27        Self {
28            v: [a, b, c],
29            normal,
30            outside_set: Vec::new(),
31        }
32    }
33
34    pub fn distance(&self, p: Vec3, points: &[Vec3]) -> f32 {
35        (p - points[self.v[0]]).dot(self.normal)
36    }
37}
38
39pub struct ConvexHull {
40    pub vertices: Vec<Vec3>,
41    pub faces: Vec<[u32; 3]>,
42}
43
44/// Computes an exact 3D Convex Hull using the Quickhull algorithm.
45pub fn compute_convex_hull(points: &[Vec3]) -> ConvexHull {
46    if points.len() < 4 {
47        // Fallback for very small point sets
48        return ConvexHull {
49            vertices: points.to_vec(),
50            faces: vec![],
51        };
52    }
53
54    let pts = points;
55
56    // 1. Find 6 extreme points
57    let mut min_x = 0;
58    let mut max_x = 0;
59    let mut min_y = 0;
60    let mut max_y = 0;
61    let mut min_z = 0;
62    let mut max_z = 0;
63
64    for i in 1..pts.len() {
65        let p = pts[i];
66        if p.x < pts[min_x].x {
67            min_x = i;
68        }
69        if p.x > pts[max_x].x {
70            max_x = i;
71        }
72        if p.y < pts[min_y].y {
73            min_y = i;
74        }
75        if p.y > pts[max_y].y {
76            max_y = i;
77        }
78        if p.z < pts[min_z].z {
79            min_z = i;
80        }
81        if p.z > pts[max_z].z {
82            max_z = i;
83        }
84    }
85
86    let extremes = [min_x, max_x, min_y, max_y, min_z, max_z];
87
88    // Find the pair furthest apart
89    let mut max_dist_sq = -1.0;
90    let mut p0 = 0;
91    let mut p1 = 0;
92    for &e1 in &extremes {
93        for &e2 in &extremes {
94            let dist_sq = (pts[e1] - pts[e2]).length_squared();
95            if dist_sq > max_dist_sq {
96                max_dist_sq = dist_sq;
97                p0 = e1;
98                p1 = e2;
99            }
100        }
101    }
102
103    if max_dist_sq < 1e-8 {
104        return ConvexHull {
105            vertices: vec![pts[0]],
106            faces: vec![],
107        };
108    }
109
110    // Find p2 furthest from line p0-p1
111    let mut max_dist_line = -1.0;
112    let mut p2 = 0;
113    let line_dir = (pts[p1] - pts[p0]).normalize();
114    for i in 0..pts.len() {
115        let p = pts[i];
116        let v = p - pts[p0];
117        let proj = v.dot(line_dir);
118        let dist_sq = (v - line_dir * proj).length_squared();
119        if dist_sq > max_dist_line {
120            max_dist_line = dist_sq;
121            p2 = i;
122        }
123    }
124
125    if max_dist_line < 1e-8 {
126        return ConvexHull {
127            vertices: vec![pts[p0], pts[p1]],
128            faces: vec![],
129        };
130    }
131
132    // Find p3 furthest from plane p0-p1-p2
133    let plane_normal = (pts[p1] - pts[p0]).cross(pts[p2] - pts[p0]).normalize();
134    let mut max_dist_plane = -1.0;
135    let mut p3 = 0;
136    for i in 0..pts.len() {
137        let dist = (pts[i] - pts[p0]).dot(plane_normal).abs();
138        if dist > max_dist_plane {
139            max_dist_plane = dist;
140            p3 = i;
141        }
142    }
143
144    if max_dist_plane < 1e-8 {
145        // Points are coplanar, return planar points
146        let mut dedup = pts.to_vec();
147        dedup.dedup_by(|a, b| {
148            (a.x - b.x).abs() < 1e-4 && (a.y - b.y).abs() < 1e-4 && (a.z - b.z).abs() < 1e-4
149        });
150        return ConvexHull {
151            vertices: dedup,
152            faces: vec![],
153        };
154    }
155
156    // Build initial tetrahedron faces
157    let mut faces = Vec::new();
158
159    // Ensure normal points OUTWARD.
160    // Face 0,1,2: if p3 is in front, flip it to 0,2,1
161    let mut f0 = HullFace::new(p0, p1, p2, pts);
162    if f0.distance(pts[p3], pts) > 0.0 {
163        f0 = HullFace::new(p0, p2, p1, pts);
164    }
165
166    let mut f1 = HullFace::new(p0, p3, p1, pts);
167    if f1.distance(pts[p2], pts) > 0.0 {
168        f1 = HullFace::new(p0, p1, p3, pts);
169    }
170
171    let mut f2 = HullFace::new(p1, p3, p2, pts);
172    if f2.distance(pts[p0], pts) > 0.0 {
173        f2 = HullFace::new(p1, p2, p3, pts);
174    }
175
176    let mut f3 = HullFace::new(p2, p3, p0, pts);
177    if f3.distance(pts[p1], pts) > 0.0 {
178        f3 = HullFace::new(p2, p0, p3, pts);
179    }
180
181    faces.push(f0);
182    faces.push(f1);
183    faces.push(f2);
184    faces.push(f3);
185
186    // Assign points to initial faces
187    for i in 0..pts.len() {
188        if i == p0 || i == p1 || i == p2 || i == p3 {
189            continue;
190        }
191        let p = pts[i];
192
193        let mut best_face = usize::MAX;
194        let mut max_d = EPSILON;
195
196        for (f_idx, f) in faces.iter().enumerate() {
197            let d = f.distance(p, pts);
198            if d > max_d {
199                max_d = d;
200                best_face = f_idx;
201            }
202        }
203
204        if best_face != usize::MAX {
205            faces[best_face].outside_set.push(i);
206        }
207    }
208
209    // Main Quickhull Loop
210    loop {
211        // Find a face with an active outside set
212        let mut active_face_idx = usize::MAX;
213        for (i, f) in faces.iter().enumerate() {
214            if !f.outside_set.is_empty() {
215                active_face_idx = i;
216                break;
217            }
218        }
219
220        if active_face_idx == usize::MAX {
221            break; // Finished!
222        }
223
224        // Pick the furthest point in the outside set
225        let mut best_pt = 0;
226        let mut max_d = -1.0;
227        let active_face = &faces[active_face_idx];
228
229        for &idx in &active_face.outside_set {
230            let d = active_face.distance(pts[idx], pts);
231            if d > max_d {
232                max_d = d;
233                best_pt = idx;
234            }
235        }
236
237        let p = pts[best_pt];
238
239        // Find all visible faces
240        let mut visible_faces = Vec::new();
241        for (i, f) in faces.iter().enumerate() {
242            if f.distance(p, pts) > EPSILON {
243                visible_faces.push(i);
244            }
245        }
246
247        // Extract horizon edges
248        // A directed edge (u, v) is added. If a neighboring visible face adds (v, u), they cancel out.
249        let mut edge_counts = HashMap::new();
250        for &f_idx in &visible_faces {
251            let f = &faces[f_idx];
252            let edges = [(f.v[0], f.v[1]), (f.v[1], f.v[2]), (f.v[2], f.v[0])];
253            for &(u, v) in &edges {
254                *edge_counts.entry((u, v)).or_insert(0) += 1;
255            }
256        }
257
258        let mut horizon_edges = Vec::new();
259        for (&(u, v), &count) in &edge_counts {
260            if count == 1 && !edge_counts.contains_key(&(v, u)) {
261                horizon_edges.push((u, v));
262            }
263        }
264
265        // Collect all points from the outside sets of visible faces
266        let mut orphaned_points = Vec::new();
267        for &f_idx in &visible_faces {
268            for &idx in &faces[f_idx].outside_set {
269                if idx != best_pt {
270                    orphaned_points.push(idx);
271                }
272            }
273        }
274
275        // Delete visible faces (mark by removing them later, for now we will build a new face list)
276        let mut new_faces = Vec::new();
277        for (i, f) in faces.into_iter().enumerate() {
278            if !visible_faces.contains(&i) {
279                new_faces.push(f);
280            }
281        }
282        faces = new_faces;
283
284        // Create new faces from horizon edges to P
285        let mut new_face_indices = Vec::new();
286        for &(u, v) in &horizon_edges {
287            let new_face = HullFace::new(u, v, best_pt, pts);
288            faces.push(new_face);
289            new_face_indices.push(faces.len() - 1);
290        }
291
292        // Reassign orphaned points
293        for idx in orphaned_points {
294            let pt = pts[idx];
295            let mut best_face = usize::MAX;
296            let mut best_d = EPSILON;
297
298            for &f_idx in &new_face_indices {
299                let d = faces[f_idx].distance(pt, pts);
300                if d > best_d {
301                    best_d = d;
302                    best_face = f_idx;
303                }
304            }
305
306            if best_face != usize::MAX {
307                faces[best_face].outside_set.push(idx);
308            }
309        }
310    }
311
312    // Extract unique vertices and mapped indices
313    let mut vertex_set = HashSet::new();
314    for f in &faces {
315        vertex_set.insert(f.v[0]);
316        vertex_set.insert(f.v[1]);
317        vertex_set.insert(f.v[2]);
318    }
319
320    let mut out_vertices = Vec::new();
321    let mut old_to_new = HashMap::new();
322
323    for old_idx in vertex_set {
324        out_vertices.push(pts[old_idx]);
325        old_to_new.insert(old_idx, (out_vertices.len() - 1) as u32);
326    }
327
328    let mut out_faces = Vec::new();
329    for f in &faces {
330        out_faces.push([
331            old_to_new[&f.v[0]],
332            old_to_new[&f.v[1]],
333            old_to_new[&f.v[2]],
334        ]);
335    }
336
337    ConvexHull {
338        vertices: out_vertices,
339        faces: out_faces,
340    }
341}