parry3d_f64/shape/
polygonal_feature3d.rs

1use crate::math::{Pose, Vector};
2use crate::shape::{PackedFeatureId, Segment, Triangle};
3#[cfg(feature = "alloc")]
4use crate::{
5    math::{Real, Vector2},
6    query::details::closest_points_segment_segment_2d,
7    query::{ContactManifold, TrackedContact},
8    utils::WBasis,
9};
10
11/// A polygonal feature representing the local polygonal approximation of
12/// a vertex, face, or edge of a convex shape.
13#[derive(Debug, Clone)]
14pub struct PolygonalFeature {
15    /// Up to four vertices forming this polygonal feature.
16    pub vertices: [Vector; 4],
17    /// The feature IDs of this polygon's vertices.
18    pub vids: [PackedFeatureId; 4],
19    /// The feature IDs of this polygon's edges.
20    pub eids: [PackedFeatureId; 4],
21    /// The feature ID of this polygonal feature.
22    pub fid: PackedFeatureId,
23    /// The number of vertices on this polygon (must be <= 4).
24    pub num_vertices: usize,
25}
26
27impl Default for PolygonalFeature {
28    fn default() -> Self {
29        Self {
30            vertices: [Vector::ZERO; 4],
31            vids: [PackedFeatureId::UNKNOWN; 4],
32            eids: [PackedFeatureId::UNKNOWN; 4],
33            fid: PackedFeatureId::UNKNOWN,
34            num_vertices: 0,
35        }
36    }
37}
38
39impl From<Triangle> for PolygonalFeature {
40    fn from(tri: Triangle) -> Self {
41        Self {
42            vertices: [tri.a, tri.b, tri.c, tri.c],
43            vids: PackedFeatureId::vertices([0, 1, 2, 2]),
44            eids: PackedFeatureId::edges([0, 1, 2, 2]),
45            fid: PackedFeatureId::face(0),
46            num_vertices: 3,
47        }
48    }
49}
50
51impl From<Segment> for PolygonalFeature {
52    fn from(seg: Segment) -> Self {
53        Self {
54            vertices: [seg.a, seg.b, seg.b, seg.b],
55            vids: PackedFeatureId::vertices([0, 1, 1, 1]),
56            eids: PackedFeatureId::edges([0, 0, 0, 0]),
57            fid: PackedFeatureId::face(0),
58            num_vertices: 2,
59        }
60    }
61}
62
63impl PolygonalFeature {
64    /// Creates a new empty polygonal feature.
65    pub fn new() -> Self {
66        Self::default()
67    }
68
69    /// Transform each vertex of this polygonal feature by the given position `pos`.
70    pub fn transform_by(&mut self, pos: &Pose) {
71        for p in &mut self.vertices[0..self.num_vertices] {
72            *p = pos * *p;
73        }
74    }
75
76    /// Computes all the contacts between two polygonal features.
77    #[cfg(feature = "alloc")]
78    pub fn contacts<ManifoldData, ContactData: Default + Copy>(
79        pos12: &Pose,
80        _pos21: &Pose,
81        sep_axis1: Vector,
82        _sep_axis2: Vector,
83        feature1: &Self,
84        feature2: &Self,
85        manifold: &mut ContactManifold<ManifoldData, ContactData>,
86        flipped: bool,
87    ) {
88        match (feature1.num_vertices, feature2.num_vertices) {
89            (2, 2) => {
90                Self::contacts_edge_edge(pos12, feature1, sep_axis1, feature2, manifold, flipped)
91            }
92            _ => Self::contacts_face_face(pos12, feature1, sep_axis1, feature2, manifold, flipped),
93        }
94    }
95
96    #[cfg(feature = "alloc")]
97    fn contacts_edge_edge<ManifoldData, ContactData: Default + Copy>(
98        pos12: &Pose,
99        face1: &PolygonalFeature,
100        sep_axis1: Vector,
101        face2: &PolygonalFeature,
102        manifold: &mut ContactManifold<ManifoldData, ContactData>,
103        flipped: bool,
104    ) {
105        // Project the faces to a 2D plane for contact clipping.
106        // The plane they are projected onto has normal sep_axis1
107        // and contains the origin (this is numerically OK because
108        // we are not working in world-space here).
109        let basis = sep_axis1.orthonormal_basis();
110        let projected_edge1 = [
111            Vector2::new(
112                face1.vertices[0].dot(basis[0]),
113                face1.vertices[0].dot(basis[1]),
114            ),
115            Vector2::new(
116                face1.vertices[1].dot(basis[0]),
117                face1.vertices[1].dot(basis[1]),
118            ),
119        ];
120
121        let vertices2_1 = [pos12 * face2.vertices[0], pos12 * face2.vertices[1]];
122        let projected_edge2 = [
123            Vector2::new(vertices2_1[0].dot(basis[0]), vertices2_1[0].dot(basis[1])),
124            Vector2::new(vertices2_1[1].dot(basis[0]), vertices2_1[1].dot(basis[1])),
125        ];
126
127        let tangent1 = (projected_edge1[1] - projected_edge1[0]).try_normalize();
128        let tangent2 = (projected_edge2[1] - projected_edge2[0]).try_normalize();
129
130        // TODO: not sure what the best value for eps is.
131        // Empirically, it appears that an epsilon smaller than 1.0e-3 is too small.
132        if let (Some(tangent1), Some(tangent2)) = (tangent1, tangent2) {
133            let parallel = tangent1.dot(tangent2) >= crate::utils::COS_FRAC_PI_8;
134
135            if !parallel {
136                let seg1 = (&projected_edge1[0], &projected_edge1[1]);
137                let seg2 = (&projected_edge2[0], &projected_edge2[1]);
138                let (loc1, loc2) = closest_points_segment_segment_2d(seg1, seg2);
139
140                // Found a contact between the two edges.
141                let bcoords1 = loc1.barycentric_coordinates();
142                let bcoords2 = loc2.barycentric_coordinates();
143
144                let edge1 = (face1.vertices[0], face1.vertices[1]);
145                let edge2 = (vertices2_1[0], vertices2_1[1]);
146                let local_p1 = edge1.0 * bcoords1[0] + edge1.1 * bcoords1[1];
147                let local_p2_1 = edge2.0 * bcoords2[0] + edge2.1 * bcoords2[1];
148                let dist = (local_p2_1 - local_p1).dot(sep_axis1);
149
150                manifold.points.push(TrackedContact::flipped(
151                    local_p1,
152                    pos12.inverse_transform_point(local_p2_1),
153                    face1.eids[0],
154                    face2.eids[0],
155                    dist,
156                    flipped,
157                ));
158
159                return;
160            }
161        }
162
163        // The lines are parallel so we are having a conformal contact.
164        // Let's use a range-based clipping to extract two contact points.
165        // TODO: would it be better and/or more efficient to do the
166        //clipping in 2D?
167        if let Some(clips) = crate::query::details::clip_segment_segment(
168            (face1.vertices[0], face1.vertices[1]),
169            (vertices2_1[0], vertices2_1[1]),
170        ) {
171            let feature_at = |face: &PolygonalFeature, id| match id {
172                0 => face.vids[0],
173                1 => face.eids[0],
174                2 => face.vids[1],
175                _ => unreachable!(),
176            };
177
178            manifold.points.push(TrackedContact::flipped(
179                (clips.0).0,
180                pos12.inverse_transform_point((clips.0).1),
181                feature_at(face1, (clips.0).2),
182                feature_at(face2, (clips.0).3),
183                ((clips.0).1 - (clips.0).0).dot(sep_axis1),
184                flipped,
185            ));
186
187            manifold.points.push(TrackedContact::flipped(
188                (clips.1).0,
189                pos12.inverse_transform_point((clips.1).1),
190                feature_at(face1, (clips.1).2),
191                feature_at(face2, (clips.1).3),
192                ((clips.1).1 - (clips.1).0).dot(sep_axis1),
193                flipped,
194            ));
195        }
196    }
197
198    #[cfg(feature = "alloc")]
199    fn contacts_face_face<ManifoldData, ContactData: Default + Copy>(
200        pos12: &Pose,
201        face1: &PolygonalFeature,
202        sep_axis1: Vector,
203        face2: &PolygonalFeature,
204        manifold: &mut ContactManifold<ManifoldData, ContactData>,
205        flipped: bool,
206    ) {
207        // Project the faces to a 2D plane for contact clipping.
208        // The plane they are projected onto has normal sep_axis1
209        // and contains the origin (this is numerically OK because
210        // we are not working in world-space here).
211        let basis = sep_axis1.orthonormal_basis();
212        let projected_face1 = [
213            Vector2::new(
214                face1.vertices[0].dot(basis[0]),
215                face1.vertices[0].dot(basis[1]),
216            ),
217            Vector2::new(
218                face1.vertices[1].dot(basis[0]),
219                face1.vertices[1].dot(basis[1]),
220            ),
221            Vector2::new(
222                face1.vertices[2].dot(basis[0]),
223                face1.vertices[2].dot(basis[1]),
224            ),
225            Vector2::new(
226                face1.vertices[3].dot(basis[0]),
227                face1.vertices[3].dot(basis[1]),
228            ),
229        ];
230
231        let vertices2_1 = [
232            pos12 * face2.vertices[0],
233            pos12 * face2.vertices[1],
234            pos12 * face2.vertices[2],
235            pos12 * face2.vertices[3],
236        ];
237        let projected_face2 = [
238            Vector2::new(vertices2_1[0].dot(basis[0]), vertices2_1[0].dot(basis[1])),
239            Vector2::new(vertices2_1[1].dot(basis[0]), vertices2_1[1].dot(basis[1])),
240            Vector2::new(vertices2_1[2].dot(basis[0]), vertices2_1[2].dot(basis[1])),
241            Vector2::new(vertices2_1[3].dot(basis[0]), vertices2_1[3].dot(basis[1])),
242        ];
243
244        // Also find all the vertices located inside of the other projected face.
245        if face2.num_vertices > 2 {
246            let normal2_1 =
247                (vertices2_1[2] - vertices2_1[1]).cross(vertices2_1[0] - vertices2_1[1]);
248            let denom = normal2_1.dot(sep_axis1);
249
250            if !relative_eq!(denom, 0.0) {
251                let last_index2 = face2.num_vertices - 1;
252
253                #[allow(clippy::needless_range_loop)] // Would make it much more verbose.
254                'point_loop1: for i in 0..face1.num_vertices {
255                    let p1 = projected_face1[i];
256
257                    let mut sign = (projected_face2[0] - projected_face2[last_index2])
258                        .perp_dot(p1 - projected_face2[last_index2]);
259                    for j in 0..last_index2 {
260                        let new_sign = (projected_face2[j + 1] - projected_face2[j])
261                            .perp_dot(p1 - projected_face2[j]);
262
263                        if sign == 0.0 {
264                            sign = new_sign;
265                        } else if sign * new_sign < 0.0 {
266                            // The point lies outside.
267                            continue 'point_loop1;
268                        }
269                    }
270
271                    // All the perp had the same sign: the point is inside of the other shapes projection.
272                    // Output the contact.
273                    let dist = (vertices2_1[0] - face1.vertices[i]).dot(normal2_1) / denom;
274                    let local_p1 = face1.vertices[i];
275                    let local_p2_1 = face1.vertices[i] + dist * sep_axis1;
276
277                    manifold.points.push(TrackedContact::flipped(
278                        local_p1,
279                        pos12.inverse_transform_point(local_p2_1),
280                        face1.vids[i],
281                        face2.fid,
282                        dist,
283                        flipped,
284                    ));
285                }
286            }
287        }
288
289        if face1.num_vertices > 2 {
290            let normal1 = (face1.vertices[2] - face1.vertices[1])
291                .cross(face1.vertices[0] - face1.vertices[1]);
292
293            let denom = -normal1.dot(sep_axis1);
294            if !relative_eq!(denom, 0.0) {
295                let last_index1 = face1.num_vertices - 1;
296                'point_loop2: for i in 0..face2.num_vertices {
297                    let p2 = projected_face2[i];
298
299                    let mut sign = (projected_face1[0] - projected_face1[last_index1])
300                        .perp_dot(p2 - projected_face1[last_index1]);
301                    for j in 0..last_index1 {
302                        let new_sign = (projected_face1[j + 1] - projected_face1[j])
303                            .perp_dot(p2 - projected_face1[j]);
304
305                        if sign == 0.0 {
306                            sign = new_sign;
307                        } else if sign * new_sign < 0.0 {
308                            // The point lies outside.
309                            continue 'point_loop2;
310                        }
311                    }
312
313                    // All the perp had the same sign: the point is inside of the other shapes projection.
314                    // Output the contact.
315                    let dist = (face1.vertices[0] - vertices2_1[i]).dot(normal1) / denom;
316                    let local_p2_1 = vertices2_1[i];
317                    let local_p1 = vertices2_1[i] - dist * sep_axis1;
318
319                    manifold.points.push(TrackedContact::flipped(
320                        local_p1,
321                        pos12.inverse_transform_point(local_p2_1),
322                        face1.fid,
323                        face2.vids[i],
324                        dist,
325                        flipped,
326                    ));
327                }
328            }
329        }
330
331        // Now we have to compute the intersection between all pairs of
332        // edges from the face 1 and from the face2.
333        for j in 0..face2.num_vertices {
334            let projected_edge2 = [
335                projected_face2[j],
336                projected_face2[(j + 1) % face2.num_vertices],
337            ];
338
339            for i in 0..face1.num_vertices {
340                let projected_edge1 = [
341                    projected_face1[i],
342                    projected_face1[(i + 1) % face1.num_vertices],
343                ];
344                if let Some(bcoords) = closest_points_line2d(projected_edge1, projected_edge2) {
345                    if bcoords.0 > 0.0 && bcoords.0 < 1.0 && bcoords.1 > 0.0 && bcoords.1 < 1.0 {
346                        // Found a contact between the two edges.
347                        let edge1 = (
348                            face1.vertices[i],
349                            face1.vertices[(i + 1) % face1.num_vertices],
350                        );
351                        let edge2 = (vertices2_1[j], vertices2_1[(j + 1) % face2.num_vertices]);
352                        let local_p1 = edge1.0 * (1.0 - bcoords.0) + edge1.1 * bcoords.0;
353                        let local_p2_1 = edge2.0 * (1.0 - bcoords.1) + edge2.1 * bcoords.1;
354                        let dist = (local_p2_1 - local_p1).dot(sep_axis1);
355
356                        manifold.points.push(TrackedContact::flipped(
357                            local_p1,
358                            pos12.inverse_transform_point(local_p2_1),
359                            face1.eids[i],
360                            face2.eids[j],
361                            dist,
362                            flipped,
363                        ));
364                    }
365                }
366            }
367        }
368    }
369}
370
371/// Compute the barycentric coordinates of the intersection between the two given lines.
372/// Returns `None` if the lines are parallel.
373#[cfg(feature = "alloc")]
374fn closest_points_line2d(edge1: [Vector2; 2], edge2: [Vector2; 2]) -> Option<(Real, Real)> {
375    use crate::approx::AbsDiffEq;
376
377    // Inspired by Real-time collision detection by Christer Ericson.
378    let dir1 = edge1[1] - edge1[0];
379    let dir2 = edge2[1] - edge2[0];
380    let r = edge1[0] - edge2[0];
381
382    let a = dir1.length_squared();
383    let e = dir2.length_squared();
384    let f = dir2.dot(r);
385
386    let eps = Real::default_epsilon();
387
388    if a <= eps && e <= eps {
389        Some((0.0, 0.0))
390    } else if a <= eps {
391        Some((0.0, f / e))
392    } else {
393        let c = dir1.dot(r);
394        if e <= eps {
395            Some((-c / a, 0.0))
396        } else {
397            let b = dir1.dot(dir2);
398            let ae = a * e;
399            let bb = b * b;
400            let denom = ae - bb;
401
402            // Use absolute and ulps error to test collinearity.
403            let parallel = denom <= eps || approx::ulps_eq!(ae, bb);
404
405            let s = if !parallel {
406                (b * f - c * e) / denom
407            } else {
408                0.0
409            };
410
411            if parallel {
412                None
413            } else {
414                Some((s, (b * s + f) / e))
415            }
416        }
417    }
418}