Skip to main content

phyz_collision/
geometry.rs

1//! Geometry primitives for collision detection.
2
3use phyz_math::{Mat3, Vec3};
4
5/// Collision geometry types.
6#[derive(Debug, Clone)]
7pub enum Geometry {
8    /// Sphere with given radius.
9    Sphere {
10        /// Ball radius, in metres.
11        radius: f64,
12    },
13    /// Capsule: cylinder with hemispheres at ends.
14    Capsule {
15        /// Radius of the swept sphere, in metres.
16        radius: f64,
17        /// Distance between the two cap centres, in metres.
18        length: f64,
19    },
20    /// Box with half-extents (width/2, height/2, depth/2).
21    Box {
22        /// Half-extent along each local axis, in metres.
23        half_extents: Vec3,
24    },
25    /// Cylinder along Z axis.
26    Cylinder {
27        /// Cylinder radius, in metres.
28        radius: f64,
29        /// Cylinder height along Z, in metres.
30        height: f64,
31    },
32    /// Convex mesh (vertices only; convex hull assumed).
33    Mesh {
34        /// Vertex positions in the body frame.
35        vertices: Vec<Vec3>,
36        /// Triangles, as triples of indices into `vertices`.
37        faces: Vec<[usize; 3]>,
38    },
39    /// Infinite plane with outward normal.
40    Plane {
41        /// Outward unit normal of the half-space, in the body frame.
42        normal: Vec3,
43    },
44}
45
46/// Axis-aligned bounding box.
47#[derive(Debug, Clone, Copy)]
48pub struct AABB {
49    /// Lower corner (componentwise minimum).
50    pub min: Vec3,
51    /// Upper corner (componentwise maximum).
52    pub max: Vec3,
53}
54
55impl AABB {
56    /// Create AABB from min/max corners.
57    pub fn new(min: Vec3, max: Vec3) -> Self {
58        Self { min, max }
59    }
60
61    /// Grow the box by `pad` in every direction.
62    ///
63    /// A broad phase that culls on the un-padded boxes decides which pairs the
64    /// narrow phase is even allowed to see, so a margin applied only in the
65    /// narrow phase is silently capped by it: an AABB gap never exceeds the
66    /// true surface gap, so any pair inside the margin has AABBs at most
67    /// `margin` apart, and padding each by half of that is exactly enough to
68    /// keep them. Without it the margin band is unreachable for most pairs.
69    pub fn expanded(&self, pad: f64) -> Self {
70        let p = Vec3::new(pad, pad, pad);
71        Self {
72            min: self.min - p,
73            max: self.max + p,
74        }
75    }
76
77    /// Compute AABB for a geometry at given position and rotation.
78    pub fn from_geometry(geom: &Geometry, pos: &Vec3, rot: &Mat3) -> Self {
79        match geom {
80            Geometry::Sphere { radius } => {
81                let r = Vec3::new(*radius, *radius, *radius);
82                AABB::new(pos - r, pos + r)
83            }
84            Geometry::Capsule { radius, length } => {
85                let half_len = length * 0.5;
86                let r = Vec3::new(*radius, *radius, half_len + *radius);
87                // Transform by rotation
88                let corners = [
89                    *rot * Vec3::new(r.x, r.y, r.z),
90                    *rot * Vec3::new(-r.x, r.y, r.z),
91                    *rot * Vec3::new(r.x, -r.y, r.z),
92                    *rot * Vec3::new(-r.x, -r.y, r.z),
93                    *rot * Vec3::new(r.x, r.y, -r.z),
94                    *rot * Vec3::new(-r.x, r.y, -r.z),
95                    *rot * Vec3::new(r.x, -r.y, -r.z),
96                    *rot * Vec3::new(-r.x, -r.y, -r.z),
97                ];
98                let mut min = pos + corners[0];
99                let mut max = min;
100                for corner in &corners[1..] {
101                    let c = *pos + *corner;
102                    min = min.component_min(c);
103                    max = max.component_max(c);
104                }
105                AABB::new(min, max)
106            }
107            Geometry::Box { half_extents } => {
108                let corners = [
109                    *rot * Vec3::new(half_extents.x, half_extents.y, half_extents.z),
110                    *rot * Vec3::new(-half_extents.x, half_extents.y, half_extents.z),
111                    *rot * Vec3::new(half_extents.x, -half_extents.y, half_extents.z),
112                    *rot * Vec3::new(-half_extents.x, -half_extents.y, half_extents.z),
113                    *rot * Vec3::new(half_extents.x, half_extents.y, -half_extents.z),
114                    *rot * Vec3::new(-half_extents.x, half_extents.y, -half_extents.z),
115                    *rot * Vec3::new(half_extents.x, -half_extents.y, -half_extents.z),
116                    *rot * Vec3::new(-half_extents.x, -half_extents.y, -half_extents.z),
117                ];
118                let mut min = pos + corners[0];
119                let mut max = min;
120                for corner in &corners[1..] {
121                    let c = *pos + *corner;
122                    min = min.component_min(c);
123                    max = max.component_max(c);
124                }
125                AABB::new(min, max)
126            }
127            Geometry::Cylinder { radius, height } => {
128                let half_h = height * 0.5;
129                let r = Vec3::new(*radius, *radius, half_h);
130                let corners = [
131                    *rot * Vec3::new(r.x, 0.0, r.z),
132                    *rot * Vec3::new(-r.x, 0.0, r.z),
133                    *rot * Vec3::new(0.0, r.y, r.z),
134                    *rot * Vec3::new(0.0, -r.y, r.z),
135                    *rot * Vec3::new(r.x, 0.0, -r.z),
136                    *rot * Vec3::new(-r.x, 0.0, -r.z),
137                    *rot * Vec3::new(0.0, r.y, -r.z),
138                    *rot * Vec3::new(0.0, -r.y, -r.z),
139                ];
140                let mut min = pos + corners[0];
141                let mut max = min;
142                for corner in &corners[1..] {
143                    let c = *pos + *corner;
144                    min = min.component_min(c);
145                    max = max.component_max(c);
146                }
147                AABB::new(min, max)
148            }
149            Geometry::Mesh { vertices, .. } => {
150                if vertices.is_empty() {
151                    return AABB::new(*pos, *pos);
152                }
153                let v0 = *pos + *rot * vertices[0];
154                let mut min = v0;
155                let mut max = v0;
156                for v in vertices.iter().skip(1) {
157                    let vt = *pos + *rot * v;
158                    min = min.component_min(vt);
159                    max = max.component_max(vt);
160                }
161                AABB::new(min, max)
162            }
163            Geometry::Plane { .. } => {
164                // Plane is infinite; return large AABB
165                let large = 1e6;
166                AABB::new(
167                    Vec3::new(-large, -large, -large),
168                    Vec3::new(large, large, large),
169                )
170            }
171        }
172    }
173
174    /// Check if two AABBs overlap.
175    pub fn overlaps(&self, other: &AABB) -> bool {
176        self.min.x <= other.max.x
177            && self.max.x >= other.min.x
178            && self.min.y <= other.max.y
179            && self.max.y >= other.min.y
180            && self.min.z <= other.max.z
181            && self.max.z >= other.min.z
182    }
183}
184
185impl Geometry {
186    /// Support function: point on surface farthest in direction `dir`.
187    pub fn support(&self, dir: &Vec3, pos: &Vec3, rot: &Mat3) -> Vec3 {
188        match self {
189            Geometry::Sphere { radius } => {
190                let d = dir.normalize();
191                pos + d * *radius
192            }
193            Geometry::Capsule { radius, length } => {
194                let half_len = length * 0.5;
195                let axis = *rot * Vec3::z();
196                let p1 = pos + axis * half_len;
197                let p2 = pos - axis * half_len;
198                let d = dir.normalize();
199                if dir.dot(axis) > 0.0 {
200                    p1 + d * *radius
201                } else {
202                    p2 + d * *radius
203                }
204            }
205            Geometry::Box { half_extents } => {
206                let local_dir = rot.transpose() * dir;
207                let sx = if local_dir.x >= 0.0 {
208                    half_extents.x
209                } else {
210                    -half_extents.x
211                };
212                let sy = if local_dir.y >= 0.0 {
213                    half_extents.y
214                } else {
215                    -half_extents.y
216                };
217                let sz = if local_dir.z >= 0.0 {
218                    half_extents.z
219                } else {
220                    -half_extents.z
221                };
222                pos + *rot * Vec3::new(sx, sy, sz)
223            }
224            Geometry::Cylinder { radius, height } => {
225                let half_h = height * 0.5;
226                let axis = *rot * Vec3::z();
227                let local_dir = rot.transpose() * dir;
228                let radial = Vec3::new(local_dir.x, local_dir.y, 0.0);
229                let radial_norm = radial.norm();
230                let radial_dir = if radial_norm > 1e-10 {
231                    radial / radial_norm
232                } else {
233                    Vec3::x()
234                };
235                let sz = if local_dir.z >= 0.0 { half_h } else { -half_h };
236                pos + *rot * (radial_dir * *radius) + axis * sz
237            }
238            Geometry::Mesh { vertices, .. } => {
239                let mut best = pos + *rot * vertices[0];
240                let mut best_dot = dir.dot(best);
241                for v in vertices.iter().skip(1) {
242                    let vt = pos + *rot * v;
243                    let d = dir.dot(vt);
244                    if d > best_dot {
245                        best = vt;
246                        best_dot = d;
247                    }
248                }
249                best
250            }
251            Geometry::Plane { normal } => {
252                // A plane is the half-space {p : n·(p - pos) <= 0}, which is
253                // unbounded both laterally and along -n. GJK needs a bounded
254                // support, so model it as a very large slab: extend to
255                // `PLANE_EXTENT` in the tangent plane, and to `-PLANE_EXTENT`
256                // along the normal, but never *past* the surface along +n.
257                //
258                // The previous version returned `pos ± n * 1e6`, i.e. the
259                // support of a two-point set on the normal axis — a shape with
260                // no lateral extent and no interior. Every plane query it fed
261                // GJK/EPA produced a normal in an arbitrary tangent direction
262                // and a depth unrelated to the actual separation.
263                //
264                // The normal is stored in the *body* frame, so it must be
265                // rotated into the world like every other geometry here (the
266                // old code used it unrotated, so a tilted ground plane behaved
267                // as though it were level).
268                const PLANE_EXTENT: f64 = 1e3;
269                let n = *rot * *normal;
270                let n_norm = n.norm();
271                let n = if n_norm > 1e-12 {
272                    n / n_norm
273                } else {
274                    Vec3::z()
275                };
276
277                let d_n = dir.dot(n);
278                let tangent = *dir - n * d_n;
279                let t_norm = tangent.norm();
280                let lateral = if t_norm > 1e-12 {
281                    tangent * (PLANE_EXTENT / t_norm)
282                } else {
283                    Vec3::zeros()
284                };
285                let along = if d_n > 0.0 { 0.0 } else { -PLANE_EXTENT };
286                pos + lateral + n * along
287            }
288        }
289    }
290}