Skip to main content

wreck/convex_polytope/
mod.rs

1pub(crate) mod array;
2pub(crate) mod heap;
3pub(crate) mod refer;
4
5use alloc::vec::Vec;
6#[cfg(not(feature = "std"))]
7#[allow(unused_imports)]
8use crate::F32Ext;
9
10use glam::Vec3;
11use wide::f32x8;
12
13use crate::{Capsule, ConvexPolytope, Cuboid, Sphere, convex_polytope::array::ArrayConvexPolytope};
14
15/// SIMD max projection of vertices onto a normal direction.
16#[inline]
17pub(crate) fn max_projection(vertices: &[Vec3], normal: Vec3) -> f32 {
18    let nx = f32x8::splat(normal.x);
19    let ny = f32x8::splat(normal.y);
20    let nz = f32x8::splat(normal.z);
21    let mut max_val = f32x8::splat(f32::NEG_INFINITY);
22
23    let chunks = vertices.chunks_exact(8);
24    let remainder = chunks.remainder();
25
26    for chunk in chunks {
27        let mut vx = [0.0f32; 8];
28        let mut vy = [0.0f32; 8];
29        let mut vz = [0.0f32; 8];
30        for (i, v) in chunk.iter().enumerate() {
31            vx[i] = v.x;
32            vy[i] = v.y;
33            vz[i] = v.z;
34        }
35        let proj = nx * f32x8::new(vx) + ny * f32x8::new(vy) + nz * f32x8::new(vz);
36        max_val = max_val.max(proj);
37    }
38
39    let arr = max_val.to_array();
40    let mut result = arr[0];
41    for &v in &arr[1..] {
42        if v > result {
43            result = v;
44        }
45    }
46    for v in remainder {
47        let p = normal.dot(*v);
48        if p > result {
49            result = p;
50        }
51    }
52    result
53}
54
55/// SIMD min projection of vertices onto a normal direction.
56#[inline]
57pub(crate) fn min_projection(vertices: &[Vec3], normal: Vec3) -> f32 {
58    let nx = f32x8::splat(normal.x);
59    let ny = f32x8::splat(normal.y);
60    let nz = f32x8::splat(normal.z);
61    let mut min_val = f32x8::splat(f32::INFINITY);
62
63    let chunks = vertices.chunks_exact(8);
64    let remainder = chunks.remainder();
65
66    for chunk in chunks {
67        let mut vx = [0.0f32; 8];
68        let mut vy = [0.0f32; 8];
69        let mut vz = [0.0f32; 8];
70        for (i, v) in chunk.iter().enumerate() {
71            vx[i] = v.x;
72            vy[i] = v.y;
73            vz[i] = v.z;
74        }
75        let proj = nx * f32x8::new(vx) + ny * f32x8::new(vy) + nz * f32x8::new(vz);
76        min_val = min_val.min(proj);
77    }
78
79    let arr = min_val.to_array();
80    let mut result = arr[0];
81    for &v in &arr[1..] {
82        if v < result {
83            result = v;
84        }
85    }
86    for v in remainder {
87        let p = normal.dot(*v);
88        if p < result {
89            result = p;
90        }
91    }
92    result
93}
94
95fn compute_obb(vertices: &[Vec3]) -> Cuboid {
96    if vertices.is_empty() {
97        return Cuboid::new(Vec3::ZERO, [Vec3::X, Vec3::Y, Vec3::Z], [0.0; 3]);
98    }
99
100    let n = vertices.len() as f32;
101    let mean = vertices.iter().copied().sum::<Vec3>() / n;
102
103    // Compute covariance matrix
104    let mut cov = [[0.0f32; 3]; 3];
105    for v in vertices {
106        let d = *v - mean;
107        let da = [d.x, d.y, d.z];
108        for i in 0..3 {
109            for j in i..3 {
110                cov[i][j] += da[i] * da[j];
111            }
112        }
113    }
114    for i in 0..3 {
115        for j in i..3 {
116            cov[i][j] /= n;
117            if j != i {
118                cov[j][i] = cov[i][j];
119            }
120        }
121    }
122
123    // Jacobi eigenvalue iteration for 3x3 symmetric matrix
124    let axes = jacobi_eigenvectors_3x3(cov);
125
126    // Project vertices onto axes to find half-extents (SIMD)
127    let mut min_proj = [0.0f32; 3];
128    let mut max_proj = [0.0f32; 3];
129    for i in 0..3 {
130        max_proj[i] = max_projection(vertices, axes[i]);
131        min_proj[i] = min_projection(vertices, axes[i]);
132    }
133
134    let center_proj: Vec3 = Vec3::new(
135        (min_proj[0] + max_proj[0]) * 0.5,
136        (min_proj[1] + max_proj[1]) * 0.5,
137        (min_proj[2] + max_proj[2]) * 0.5,
138    );
139    let center = axes[0] * center_proj.x + axes[1] * center_proj.y + axes[2] * center_proj.z;
140    let half_extents = [
141        (max_proj[0] - min_proj[0]) * 0.5,
142        (max_proj[1] - min_proj[1]) * 0.5,
143        (max_proj[2] - min_proj[2]) * 0.5,
144    ];
145
146    Cuboid::new(center, axes, half_extents)
147}
148
149fn jacobi_eigenvectors_3x3(mut a: [[f32; 3]; 3]) -> [Vec3; 3] {
150    let mut v = [[0.0f32; 3]; 3];
151    for i in 0..3 {
152        v[i][i] = 1.0;
153    }
154
155    for _ in 0..50 {
156        // Find largest off-diagonal element
157        let mut p = 0;
158        let mut q = 1;
159        let mut max_val = a[0][1].abs();
160        for i in 0..3 {
161            for j in (i + 1)..3 {
162                if a[i][j].abs() > max_val {
163                    max_val = a[i][j].abs();
164                    p = i;
165                    q = j;
166                }
167            }
168        }
169
170        if max_val < 1e-10 {
171            break;
172        }
173
174        let theta = 0.5 * (a[q][q] - a[p][p]).atan2(a[p][q]);
175        let c = theta.cos();
176        let s = theta.sin();
177
178        // Apply rotation to A
179        let mut new_a = a;
180        for i in 0..3 {
181            new_a[i][p] = c * a[i][p] + s * a[i][q];
182            new_a[i][q] = -s * a[i][p] + c * a[i][q];
183        }
184        a = new_a;
185        let mut new_a = a;
186        for j in 0..3 {
187            new_a[p][j] = c * a[p][j] + s * a[q][j];
188            new_a[q][j] = -s * a[p][j] + c * a[q][j];
189        }
190        a = new_a;
191
192        // Apply rotation to V
193        let mut new_v = v;
194        for i in 0..3 {
195            new_v[i][p] = c * v[i][p] + s * v[i][q];
196            new_v[i][q] = -s * v[i][p] + c * v[i][q];
197        }
198        v = new_v;
199    }
200
201    [
202        Vec3::new(v[0][0], v[1][0], v[2][0]).normalize_or_zero(),
203        Vec3::new(v[0][1], v[1][1], v[2][1]).normalize_or_zero(),
204        Vec3::new(v[0][2], v[1][2], v[2][2]).normalize_or_zero(),
205    ]
206}
207
208use crate::Collides;
209use refer::RefConvexPolytope;
210
211impl<const P: usize, const V: usize> Collides<ConvexPolytope> for ArrayConvexPolytope<P, V> {
212    #[inline]
213    fn test<const BROADPHASE: bool>(&self, other: &ConvexPolytope) -> bool {
214        RefConvexPolytope::from_array(self)
215            .collides_polytope::<BROADPHASE>(&RefConvexPolytope::from_heap(other))
216    }
217}
218
219impl<const P: usize, const V: usize> Collides<ArrayConvexPolytope<P, V>> for ConvexPolytope {
220    #[inline]
221    fn test<const BROADPHASE: bool>(&self, other: &ArrayConvexPolytope<P, V>) -> bool {
222        RefConvexPolytope::from_heap(self)
223            .collides_polytope::<BROADPHASE>(&RefConvexPolytope::from_array(other))
224    }
225}
226
227/// Approximate a sphere as a convex polytope using an icosphere with 42 vertices.
228impl From<Sphere> for ConvexPolytope {
229    fn from(sphere: Sphere) -> Self {
230        // Generate icosphere vertices (12 base + 30 edge midpoints = 42 vertices)
231        let phi = (1.0 + 5.0_f32.sqrt()) / 2.0;
232        let len = (1.0 + phi * phi).sqrt();
233        let a = 1.0 / len;
234        let b = phi / len;
235
236        // 12 icosahedron vertices (normalized to unit sphere)
237        let ico = [
238            Vec3::new(-a, b, 0.0),
239            Vec3::new(a, b, 0.0),
240            Vec3::new(-a, -b, 0.0),
241            Vec3::new(a, -b, 0.0),
242            Vec3::new(0.0, -a, b),
243            Vec3::new(0.0, a, b),
244            Vec3::new(0.0, -a, -b),
245            Vec3::new(0.0, a, -b),
246            Vec3::new(b, 0.0, -a),
247            Vec3::new(b, 0.0, a),
248            Vec3::new(-b, 0.0, -a),
249            Vec3::new(-b, 0.0, a),
250        ];
251
252        // 20 icosahedron faces (indices)
253        let faces: [[usize; 3]; 20] = [
254            [0, 11, 5],
255            [0, 5, 1],
256            [0, 1, 7],
257            [0, 7, 10],
258            [0, 10, 11],
259            [1, 5, 9],
260            [5, 11, 4],
261            [11, 10, 2],
262            [10, 7, 6],
263            [7, 1, 8],
264            [3, 9, 4],
265            [3, 4, 2],
266            [3, 2, 6],
267            [3, 6, 8],
268            [3, 8, 9],
269            [4, 9, 5],
270            [2, 4, 11],
271            [6, 2, 10],
272            [8, 6, 7],
273            [9, 8, 1],
274        ];
275
276        // Subdivide once: each face -> 4 faces, project midpoints onto unit sphere
277        let mut vertices = Vec::new();
278        let mut normals_set: Vec<Vec3> = Vec::new();
279
280        #[cfg(feature = "std")]
281        let mut get_or_insert = {
282            let mut vert_map = std::collections::HashMap::new();
283            move |v: Vec3, verts: &mut Vec<Vec3>| -> usize {
284                let key = ((v.x * 1e5) as i32, (v.y * 1e5) as i32, (v.z * 1e5) as i32);
285                *vert_map.entry(key).or_insert_with(|| {
286                    let idx = verts.len();
287                    verts.push(v);
288                    idx
289                })
290            }
291        };
292
293        #[cfg(not(feature = "std"))]
294        let get_or_insert = |v: Vec3, verts: &mut Vec<Vec3>| -> usize {
295            let key = ((v.x * 1e5) as i32, (v.y * 1e5) as i32, (v.z * 1e5) as i32);
296            for (i, existing) in verts.iter().enumerate() {
297                let ek = (
298                    (existing.x * 1e5) as i32,
299                    (existing.y * 1e5) as i32,
300                    (existing.z * 1e5) as i32,
301                );
302                if ek == key {
303                    return i;
304                }
305            }
306            let idx = verts.len();
307            verts.push(v);
308            idx
309        };
310
311        let mut sub_faces: Vec<[usize; 3]> = Vec::new();
312        for face in &faces {
313            let v0 = ico[face[0]];
314            let v1 = ico[face[1]];
315            let v2 = ico[face[2]];
316            let m01 = ((v0 + v1) * 0.5).normalize();
317            let m12 = ((v1 + v2) * 0.5).normalize();
318            let m20 = ((v2 + v0) * 0.5).normalize();
319
320            let i0 = get_or_insert(v0, &mut vertices);
321            let i1 = get_or_insert(v1, &mut vertices);
322            let i2 = get_or_insert(v2, &mut vertices);
323            let i01 = get_or_insert(m01, &mut vertices);
324            let i12 = get_or_insert(m12, &mut vertices);
325            let i20 = get_or_insert(m20, &mut vertices);
326
327            sub_faces.push([i0, i01, i20]);
328            sub_faces.push([i01, i1, i12]);
329            sub_faces.push([i20, i12, i2]);
330            sub_faces.push([i01, i12, i20]);
331        }
332
333        // Compute face normals as plane normals
334        for face in &sub_faces {
335            let v0 = vertices[face[0]];
336            let v1 = vertices[face[1]];
337            let v2 = vertices[face[2]];
338            let n = (v1 - v0).cross(v2 - v0);
339            if n.length_squared() > 1e-10 {
340                let n = n.normalize();
341                // Ensure outward-facing
342                let n = if n.dot(v0) > 0.0 { n } else { -n };
343                if !normals_set.iter().any(|existing| existing.dot(n) > 0.9999) {
344                    normals_set.push(n);
345                }
346            }
347        }
348
349        // Scale and translate vertices
350        let scaled_verts: Vec<Vec3> = vertices
351            .iter()
352            .map(|v| sphere.center + *v * sphere.radius)
353            .collect();
354
355        // Build planes: for each normal, d = n·center + radius (since vertices are on the sphere surface)
356        let planes: Vec<(Vec3, f32)> = normals_set
357            .iter()
358            .map(|&n| {
359                let d = max_projection(&scaled_verts, n);
360                (n, d)
361            })
362            .collect();
363
364        let obb = Cuboid::new(
365            sphere.center,
366            [Vec3::X, Vec3::Y, Vec3::Z],
367            [sphere.radius; 3],
368        );
369
370        ConvexPolytope::with_obb(planes, scaled_verts, obb)
371    }
372}
373
374impl From<Cuboid> for ConvexPolytope {
375    fn from(cuboid: Cuboid) -> Self {
376        // 6 face normals (positive and negative for each axis)
377        let planes = vec![
378            (
379                cuboid.axes[0],
380                cuboid.axes[0].dot(cuboid.center) + cuboid.half_extents[0],
381            ),
382            (
383                -cuboid.axes[0],
384                (-cuboid.axes[0]).dot(cuboid.center) + cuboid.half_extents[0],
385            ),
386            (
387                cuboid.axes[1],
388                cuboid.axes[1].dot(cuboid.center) + cuboid.half_extents[1],
389            ),
390            (
391                -cuboid.axes[1],
392                (-cuboid.axes[1]).dot(cuboid.center) + cuboid.half_extents[1],
393            ),
394            (
395                cuboid.axes[2],
396                cuboid.axes[2].dot(cuboid.center) + cuboid.half_extents[2],
397            ),
398            (
399                -cuboid.axes[2],
400                (-cuboid.axes[2]).dot(cuboid.center) + cuboid.half_extents[2],
401            ),
402        ];
403
404        // 8 corner vertices
405        let mut vertices = Vec::with_capacity(8);
406        for &sx in &[-1.0_f32, 1.0] {
407            for &sy in &[-1.0_f32, 1.0] {
408                for &sz in &[-1.0_f32, 1.0] {
409                    vertices.push(
410                        cuboid.center
411                            + cuboid.axes[0] * (sx * cuboid.half_extents[0])
412                            + cuboid.axes[1] * (sy * cuboid.half_extents[1])
413                            + cuboid.axes[2] * (sz * cuboid.half_extents[2]),
414                    );
415                }
416            }
417        }
418
419        ConvexPolytope::with_obb(planes, vertices, cuboid)
420    }
421}
422
423impl From<Capsule> for ConvexPolytope {
424    fn from(capsule: Capsule) -> Self {
425        // Approximate capsule as a convex hull of two hemispheres.
426        // Use a ring of vertices around each endpoint plus the endpoints themselves.
427        let p1 = capsule.p1;
428        let p2 = capsule.p2();
429        let dir = capsule.dir;
430        let dir_len = dir.length();
431
432        // Build a local frame
433        let (ax_fwd, ax_u, ax_v) = if dir_len > 1e-6 {
434            let fwd = dir / dir_len;
435            let up = if fwd.y.abs() < 0.9 { Vec3::Y } else { Vec3::X };
436            let u = fwd.cross(up).normalize();
437            let v = u.cross(fwd);
438            (fwd, u, v)
439        } else {
440            // Degenerate capsule (point-like) → use sphere conversion
441            return ConvexPolytope::from(Sphere::new(p1, capsule.radius));
442        };
443
444        let r = capsule.radius;
445        let n_ring = 8;
446        let mut vertices = Vec::new();
447
448        // Hemisphere vertices at p1 (backward hemisphere)
449        vertices.push(p1 - ax_fwd * r); // pole
450        for i in 0..n_ring {
451            let angle = core::f32::consts::TAU * i as f32 / n_ring as f32;
452            let (sin_a, cos_a) = angle.sin_cos();
453            // Equator
454            vertices.push(p1 + (ax_u * cos_a + ax_v * sin_a) * r);
455            // 45-degree ring toward back pole
456            let lat = core::f32::consts::FRAC_PI_4;
457            vertices.push(
458                p1 - ax_fwd * (r * lat.sin()) + (ax_u * cos_a + ax_v * sin_a) * (r * lat.cos()),
459            );
460        }
461
462        // Hemisphere vertices at p2 (forward hemisphere)
463        vertices.push(p2 + ax_fwd * r); // pole
464        for i in 0..n_ring {
465            let angle = core::f32::consts::TAU * i as f32 / n_ring as f32;
466            let (sin_a, cos_a) = angle.sin_cos();
467            // Equator
468            vertices.push(p2 + (ax_u * cos_a + ax_v * sin_a) * r);
469            // 45-degree ring toward front pole
470            let lat = core::f32::consts::FRAC_PI_4;
471            vertices.push(
472                p2 + ax_fwd * (r * lat.sin()) + (ax_u * cos_a + ax_v * sin_a) * (r * lat.cos()),
473            );
474        }
475
476        // Build planes from unique outward normals
477        // End caps
478        let mut planes: Vec<(Vec3, f32)> = vec![
479            (ax_fwd, ax_fwd.dot(p2) + r),
480            (-ax_fwd, (-ax_fwd).dot(p1) + r),
481        ];
482
483        // Side planes from ring directions
484        for i in 0..n_ring {
485            let angle = core::f32::consts::TAU * i as f32 / n_ring as f32;
486            let (sin_a, cos_a) = angle.sin_cos();
487            let radial = (ax_u * cos_a + ax_v * sin_a).normalize();
488            let d = max_projection(&vertices, radial);
489            planes.push((radial, d));
490
491            // Diagonal normals (between radial and forward/backward)
492            for &blend_fwd in &[0.5_f32, -0.5] {
493                let diag = (radial + ax_fwd * blend_fwd).normalize();
494                let d = max_projection(&vertices, diag);
495                planes.push((diag, d));
496            }
497        }
498
499        let obb = compute_obb(&vertices);
500        ConvexPolytope::with_obb(planes, vertices, obb)
501    }
502}
503
504impl<const P: usize, const V: usize> From<ArrayConvexPolytope<P, V>> for ConvexPolytope {
505    fn from(polytope: ArrayConvexPolytope<P, V>) -> Self {
506        ConvexPolytope::with_obb(
507            polytope.planes.to_vec(),
508            polytope.vertices.to_vec(),
509            polytope.obb,
510        )
511    }
512}