parry3d/query/ray/
ray_trimesh.rs

1use crate::math::Real;
2use crate::query::{Ray, RayCast, RayIntersection};
3use crate::shape::{CompositeShapeRef, FeatureId, TriMesh};
4
5#[cfg(feature = "dim3")]
6pub use ray_cast_with_culling::RayCullingMode;
7
8impl RayCast for TriMesh {
9    #[inline]
10    fn cast_local_ray(&self, ray: &Ray, max_time_of_impact: Real, solid: bool) -> Option<Real> {
11        CompositeShapeRef(self)
12            .cast_local_ray(ray, max_time_of_impact, solid)
13            .map(|hit| hit.1)
14    }
15
16    #[inline]
17    fn cast_local_ray_and_get_normal(
18        &self,
19        ray: &Ray,
20        max_time_of_impact: Real,
21        solid: bool,
22    ) -> Option<RayIntersection> {
23        CompositeShapeRef(self)
24            .cast_local_ray_and_get_normal(ray, max_time_of_impact, solid)
25            .map(|(best, mut res)| {
26                // We hit a backface.
27                // NOTE: we need this for `TriMesh::is_backface` to work properly.
28                if res.feature == FeatureId::Face(1) {
29                    res.feature = FeatureId::Face(best + self.indices().len() as u32)
30                } else {
31                    res.feature = FeatureId::Face(best);
32                }
33                res
34            })
35    }
36}
37
38// NOTE: implement the ray-cast with culling on its own submodule to facilitate feature gating.
39#[cfg(feature = "dim3")]
40mod ray_cast_with_culling {
41    use crate::math::{Isometry, Real, Vector};
42    use crate::partitioning::Bvh;
43    use crate::query::details::NormalConstraints;
44    use crate::query::{Ray, RayIntersection};
45    use crate::shape::{
46        CompositeShape, CompositeShapeRef, FeatureId, Shape, TriMesh, Triangle, TypedCompositeShape,
47    };
48
49    /// Controls which side of a triangle a ray-cast is allowed to hit.
50    #[derive(Copy, Clone, PartialEq, Eq, Debug)]
51    pub enum RayCullingMode {
52        /// Ray-casting won’t hit back faces.
53        IgnoreBackfaces,
54        /// Ray-casting won’t hit front faces.
55        IgnoreFrontfaces,
56    }
57
58    impl RayCullingMode {
59        fn check(self, tri_normal: &Vector<Real>, ray_dir: &Vector<Real>) -> bool {
60            match self {
61                RayCullingMode::IgnoreBackfaces => tri_normal.dot(ray_dir) < 0.0,
62                RayCullingMode::IgnoreFrontfaces => tri_normal.dot(ray_dir) > 0.0,
63            }
64        }
65    }
66
67    /// A utility shape with a `TypedCompositeShape` implementation that skips triangles that
68    /// are back-faces or front-faces relative to a given ray and culling mode.
69    struct TriMeshWithCulling<'a> {
70        trimesh: &'a TriMesh,
71        culling: RayCullingMode,
72        ray: &'a Ray,
73    }
74
75    impl CompositeShape for TriMeshWithCulling<'_> {
76        fn map_part_at(
77            &self,
78            shape_id: u32,
79            f: &mut dyn FnMut(Option<&Isometry<Real>>, &dyn Shape, Option<&dyn NormalConstraints>),
80        ) {
81            let _ = self.map_untyped_part_at(shape_id, f);
82        }
83
84        /// Gets the acceleration structure of the composite shape.
85        fn bvh(&self) -> &Bvh {
86            self.trimesh.bvh()
87        }
88    }
89
90    impl TypedCompositeShape for TriMeshWithCulling<'_> {
91        type PartShape = Triangle;
92        type PartNormalConstraints = ();
93
94        #[inline(always)]
95        fn map_typed_part_at<T>(
96            &self,
97            i: u32,
98            mut f: impl FnMut(
99                Option<&Isometry<Real>>,
100                &Self::PartShape,
101                Option<&Self::PartNormalConstraints>,
102            ) -> T,
103        ) -> Option<T> {
104            let tri = self.trimesh.triangle(i);
105            let tri_normal = tri.scaled_normal();
106
107            if self.culling.check(&tri_normal, &self.ray.dir) {
108                Some(f(None, &tri, None))
109            } else {
110                None
111            }
112        }
113
114        #[inline(always)]
115        fn map_untyped_part_at<T>(
116            &self,
117            i: u32,
118            mut f: impl FnMut(Option<&Isometry<Real>>, &dyn Shape, Option<&dyn NormalConstraints>) -> T,
119        ) -> Option<T> {
120            let tri = self.trimesh.triangle(i);
121            let tri_normal = tri.scaled_normal();
122
123            if self.culling.check(&tri_normal, &self.ray.dir) {
124                Some(f(None, &tri, None))
125            } else {
126                None
127            }
128        }
129    }
130
131    impl TriMesh {
132        /// Casts a ray on this triangle mesh transformed by `m`.
133        ///
134        /// Hits happening later than `max_time_of_impact` are ignored. In other words, hits are
135        /// only searched along the segment `[ray.origin, ray.origin + ray.dir * max_time_of_impact`].
136        ///
137        /// Hits on back-faces or front-faces are ignored, depending on the given `culling` mode.
138        #[inline]
139        pub fn cast_ray_with_culling(
140            &self,
141            m: &Isometry<Real>,
142            ray: &Ray,
143            max_time_of_impact: Real,
144            culling: RayCullingMode,
145        ) -> Option<RayIntersection> {
146            let ls_ray = ray.inverse_transform_by(m);
147            self.cast_local_ray_with_culling(&ls_ray, max_time_of_impact, culling)
148                .map(|inter| inter.transform_by(m))
149        }
150
151        /// Casts a ray on this triangle mesh.
152        ///
153        /// This is the same as [`TriMesh::cast_ray_with_culling`] except that the ray is given
154        /// in the mesh’s local-space.
155        pub fn cast_local_ray_with_culling(
156            &self,
157            ray: &Ray,
158            max_time_of_impact: Real,
159            culling: RayCullingMode,
160        ) -> Option<RayIntersection> {
161            let mesh_with_culling = TriMeshWithCulling {
162                trimesh: self,
163                culling,
164                ray,
165            };
166            CompositeShapeRef(&mesh_with_culling)
167                .cast_local_ray_and_get_normal(ray, max_time_of_impact, false)
168                .map(|(best, mut res)| {
169                    // We hit a backface.
170                    // NOTE: we need this for `TriMesh::is_backface` to work properly.
171                    if res.feature == FeatureId::Face(1) {
172                        res.feature = FeatureId::Face(best + self.indices().len() as u32)
173                    } else {
174                        res.feature = FeatureId::Face(best);
175                    }
176                    res
177                })
178        }
179    }
180
181    #[cfg(test)]
182    mod test {
183        use crate::query::{Ray, RayCullingMode};
184        use crate::shape::TriMesh;
185        use nalgebra::{Point3, Vector3};
186
187        #[test]
188        fn cast_ray_on_trimesh_with_culling() {
189            let vertices = vec![
190                Point3::new(0.0, 0.0, 0.0),
191                Point3::new(1.0, 0.0, 0.0),
192                Point3::new(0.0, 1.0, 0.0),
193            ];
194            let indices = vec![[0, 1, 2]];
195            let ray_up = Ray::new(Point3::new(0.0, 0.0, -1.0), Vector3::new(0.0, 0.0, 1.0));
196            let ray_down = Ray::new(Point3::new(0.0, 0.0, 1.0), Vector3::new(0.0, 0.0, -1.0));
197
198            let mesh = TriMesh::new(vertices, indices).unwrap();
199            assert!(mesh
200                .cast_local_ray_with_culling(&ray_up, 1000.0, RayCullingMode::IgnoreFrontfaces)
201                .is_some());
202            assert!(mesh
203                .cast_local_ray_with_culling(&ray_down, 1000.0, RayCullingMode::IgnoreFrontfaces)
204                .is_none());
205
206            assert!(mesh
207                .cast_local_ray_with_culling(&ray_up, 1000.0, RayCullingMode::IgnoreBackfaces)
208                .is_none());
209            assert!(mesh
210                .cast_local_ray_with_culling(&ray_down, 1000.0, RayCullingMode::IgnoreBackfaces)
211                .is_some());
212        }
213    }
214}