Skip to main content

parry3d/shape/
triangle_pseudo_normals.rs

1use crate::math::Vector;
2
3#[cfg(feature = "alloc")]
4use crate::{math::Vector3, query::details::NormalConstraints};
5
6// NOTE: ideally, the normal cone should take into account the point where the normal cone is
7//       considered. But as long as we assume that the triangles are one-way we can get away with
8//       just relying on the normal directions.
9//       Taking the point into account would be technically doable (and desirable if we wanted
10//       to define, e.g., a one-way mesh) but requires:
11//       1. To make sure the edge pseudo-normals are given in the correct edge order.
12//       2. To have access to the contact feature.
13//       We can have access to both during the narrow-phase, but leave that as a future
14//       potential improvements.
15// NOTE: this isn’t equal to the "true" normal cones since concave edges will have pseudo-normals
16//       still pointing outward (instead of inward or being empty).
17/// The pseudo-normals of a triangle providing approximations of its feature’s normal cones.
18#[derive(Clone, Debug)]
19pub struct TrianglePseudoNormals {
20    /// The triangle’s face normal.
21    pub face: Vector,
22    // TODO: if we switch this to providing pseudo-normals in a specific order
23    //       (e.g. in the same order as the triangle’s edges), then we should
24    //       think of fixing that order in the heightfield
25    //       triangle_pseudo_normals code.
26    /// The edges pseudo-normals, in no particular order.
27    pub edges: [Vector; 3],
28    /// If `true`, a direction pointing to the back of the triangle is projected into the
29    /// mirrored cone (`-face`, `-edges`) and kept, instead of being discarded.
30    pub two_sided: bool,
31}
32
33#[cfg(feature = "alloc")]
34impl NormalConstraints for TrianglePseudoNormals {
35    /// Projects the given direction to it is contained in the polygonal
36    /// cone defined `self`.
37    fn project_local_normal_mut(&self, dir: &mut Vector) -> bool {
38        if self.two_sided && dir.dot(self.face) < 0.0 {
39            // The back cone is the front cone mirrored through the triangle's plane.
40            let mut mirrored = -*dir;
41            let _ = self.project_front(&mut mirrored);
42            *dir = -mirrored;
43            return true;
44        }
45
46        self.project_front(dir)
47    }
48}
49
50#[cfg(feature = "alloc")]
51impl TrianglePseudoNormals {
52    fn project_front(&self, dir: &mut Vector) -> bool {
53        // Find the closest pseudo-normal.
54        let dots = Vector3::new(
55            dir.dot(self.edges[0]),
56            dir.dot(self.edges[1]),
57            dir.dot(self.edges[2]),
58        );
59        let closest_edge = self.edges[dots.max_position()];
60        crate::shape::pseudo_normals::project_into_cone(self.face, closest_edge, dir)
61    }
62}
63
64#[cfg(test)]
65#[cfg(all(feature = "dim3", feature = "alloc"))]
66mod test {
67    use super::NormalConstraints;
68    use crate::math::{Real, Vector};
69    use crate::shape::TrianglePseudoNormals;
70
71    fn bisector(v1: Vector, v2: Vector) -> Vector {
72        (v1 + v2).normalize()
73    }
74
75    fn bisector_y(v: Vector) -> Vector {
76        bisector(v, Vector::Y)
77    }
78
79    #[test]
80    fn trivial_pseudo_normals_projection() {
81        let pn = TrianglePseudoNormals {
82            face: Vector::Y,
83            edges: [Vector::Y; 3],
84            two_sided: false,
85        };
86
87        assert_eq!(
88            pn.project_local_normal(Vector::new(1.0, 1.0, 1.0)),
89            Some(Vector::Y)
90        );
91        assert!(pn.project_local_normal(-Vector::Y).is_none());
92    }
93
94    #[test]
95    fn edge_pseudo_normals_projection_strictly_positive() {
96        let bisector = |v1: Vector, v2: Vector| (v1 + v2).normalize();
97        let bisector_y = |v: Vector| bisector(v, Vector::Y);
98
99        // The normal cones for this test will be fully contained in the +Y half-space.
100        let cones_ref_dir = [
101            -Vector::Z,
102            -Vector::X,
103            Vector::new(1.0, 0.0, 1.0).normalize(),
104        ];
105        let cones_ends = cones_ref_dir.map(bisector_y);
106        let cones_axes = cones_ends.map(bisector_y);
107
108        let pn = TrianglePseudoNormals {
109            face: Vector::Y,
110            edges: cones_axes.map(|v| v.normalize()),
111            two_sided: false,
112        };
113
114        for i in 0..3 {
115            assert!(pn
116                .project_local_normal(cones_ends[i])
117                .unwrap()
118                .abs_diff_eq(cones_ends[i], 1.0e-5));
119            assert_eq!(pn.project_local_normal(cones_axes[i]), Some(cones_axes[i]));
120
121            // Guaranteed to be inside the normal cone of edge i.
122            let subdivs = 100;
123
124            for k in 1..100 {
125                let v = Vector::Y
126                    .lerp(cones_ends[i], k as Real / (subdivs as Real))
127                    .normalize();
128                assert_eq!(pn.project_local_normal(v).unwrap(), v);
129            }
130
131            // Guaranteed to be outside the normal cone of edge i.
132            for k in 1..subdivs {
133                let v = cones_ref_dir[i]
134                    .lerp(cones_ends[i], k as Real / (subdivs as Real))
135                    .normalize();
136                assert!(pn
137                    .project_local_normal(v)
138                    .unwrap()
139                    .abs_diff_eq(cones_ends[i], 1.0e-5));
140            }
141
142            // Guaranteed to be outside the normal cone, and in the -Y half-space.
143            for k in 1..subdivs {
144                let v = cones_ref_dir[i]
145                    .lerp(-Vector::Y, k as Real / (subdivs as Real))
146                    .normalize();
147                assert!(pn.project_local_normal(v).is_none(),);
148            }
149        }
150    }
151
152    #[test]
153    fn edge_pseudo_normals_projection_negative() {
154        // The normal cones for this test will be fully contained in the +Y half-space.
155        let cones_ref_dir = [
156            -Vector::Z,
157            -Vector::X,
158            Vector::new(1.0, 0.0, 1.0).normalize(),
159        ];
160        let cones_ends = cones_ref_dir.map(|v| bisector(v, -Vector::Y));
161        let cones_axes = [
162            bisector(bisector_y(cones_ref_dir[0]), cones_ref_dir[0]),
163            bisector(bisector_y(cones_ref_dir[1]), cones_ref_dir[1]),
164            bisector(bisector_y(cones_ref_dir[2]), cones_ref_dir[2]),
165        ];
166
167        let pn = TrianglePseudoNormals {
168            face: Vector::Y,
169            edges: cones_axes.map(|v| v.normalize()),
170            two_sided: false,
171        };
172
173        for i in 0..3 {
174            assert_eq!(pn.project_local_normal(cones_axes[i]), Some(cones_axes[i]));
175
176            // Guaranteed to be inside the normal cone of edge i.
177            let subdivs = 100;
178
179            for k in 1..subdivs {
180                let v = Vector::Y
181                    .lerp(cones_ends[i], k as Real / (subdivs as Real))
182                    .normalize();
183                assert_eq!(pn.project_local_normal(v).unwrap(), v);
184            }
185
186            // Guaranteed to be outside the normal cone of edge i.
187            // Since it is additionally guaranteed to be in the -Y half-space, we should get None.
188            for k in 1..subdivs {
189                let v = (-Vector::Y)
190                    .lerp(cones_ends[i], k as Real / (subdivs as Real))
191                    .normalize();
192                assert!(pn.project_local_normal(v).is_none());
193            }
194        }
195    }
196
197    #[test]
198    fn two_sided_pseudo_normals_mirror_the_cone() {
199        let one_sided = TrianglePseudoNormals {
200            face: Vector::Y,
201            edges: [Vector::Y; 3],
202            two_sided: false,
203        };
204        let two_sided = TrianglePseudoNormals {
205            two_sided: true,
206            ..one_sided.clone()
207        };
208
209        // Front directions behave the same.
210        let front = Vector::new(1.0, 1.0, 1.0);
211        assert_eq!(two_sided.project_local_normal(front), Some(Vector::Y));
212
213        // Back directions are kept and constrained to the mirrored cone.
214        let back = Vector::new(1.0, -1.0, 1.0);
215        assert!(one_sided.project_local_normal(back).is_none());
216        assert_eq!(two_sided.project_local_normal(back), Some(-Vector::Y));
217
218        // The mirrored projection is the negation of the front projection.
219        let cone_axes = [
220            bisector_y(-Vector::Z),
221            bisector_y(-Vector::X),
222            bisector_y(Vector::new(1.0, 0.0, 1.0).normalize()),
223        ];
224        let pn = TrianglePseudoNormals {
225            face: Vector::Y,
226            edges: cone_axes,
227            two_sided: true,
228        };
229        for dir in [
230            Vector::new(0.3, 1.0, -0.8).normalize(),
231            Vector::new(-1.0, 0.2, 0.1).normalize(),
232            Vector::new(0.9, 0.5, 0.9).normalize(),
233        ] {
234            let front = pn.project_local_normal(dir).unwrap();
235            let back = pn.project_local_normal(-dir).unwrap();
236            assert!(back.abs_diff_eq(-front, 1.0e-6));
237        }
238    }
239}