phys_raycast/shapes/
triangle.rs

1// Copyright (C) 2020-2025 phys-raycast authors. All Rights Reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14use phys_geom::math::Real;
15use phys_geom::shape::Triangle;
16
17use crate::{Raycast, RaycastHitResult};
18
19impl Raycast for Triangle {
20    fn raycast(
21        &self,
22        local_ray: phys_geom::Ray,
23        max_distance: Real,
24        discard_inside_hit: bool,
25    ) -> Option<RaycastHitResult> {
26        // Möller–Trumbore ray-triangle intersection algorithm
27        let edge1 = self.b - self.a;
28        let edge2 = self.c - self.a;
29        let h = local_ray.direction.into_inner().cross(&edge2);
30        let a = edge1.dot(&h);
31
32        // Ray is parallel to the triangle
33        if a > -Real::EPSILON && a < Real::EPSILON {
34            return None;
35        }
36
37        // Backface culling: only accept hits from the front side (a > 0)
38        if a < 0.0 {
39            return None;
40        }
41
42        let f = 1.0 / a;
43        let s = local_ray.origin - self.a;
44        let u = f * s.dot(&h);
45
46        // Ray misses the triangle
47        if !(0.0..=1.0).contains(&u) {
48            return None;
49        }
50
51        let q = s.cross(&edge1);
52        let v = f * local_ray.direction.into_inner().dot(&q);
53
54        // Ray misses the triangle
55        if v < 0.0 || u + v > 1.0 {
56            return None;
57        }
58
59        let t = f * edge2.dot(&q);
60
61        // Ray intersection
62        if t > Real::EPSILON {
63            if t > max_distance {
64                return None;
65            }
66
67            // Handle inside hit (ray starts very close to the triangle)
68            if t < Real::EPSILON * 100.0 {
69                if discard_inside_hit {
70                    return None;
71                }
72                return Some(RaycastHitResult {
73                    distance: 0.0,
74                    normal: -self.normal(), // Return normal facing the ray
75                });
76            }
77
78            return Some(RaycastHitResult {
79                distance: t,
80                normal: -self.normal(), // Return normal facing the ray
81            });
82        }
83
84        None
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use approx::assert_relative_eq;
91    use phys_geom::math::*;
92
93    use super::*;
94
95    #[test]
96    fn test_raycast_triangle_basic() {
97        // Simple triangle in XY plane
98        let triangle = Triangle::new(
99            Point3::new(-1.0, 0.0, 0.0),
100            Point3::new(1.0, 0.0, 0.0),
101            Point3::new(0.0, 1.0, 0.0),
102        );
103
104        // Ray hitting from above
105        let ray = phys_geom::Ray::new(
106            Point3::new(0.0, 0.5, 2.0),
107            UnitVec3::new_normalize(Vec3::new(0.0, 0.0, -1.0)),
108        );
109
110        let hit = triangle.raycast(ray, 10.0, false).unwrap();
111        assert_relative_eq!(hit.distance, 2.0);
112        assert_relative_eq!(
113            hit.normal,
114            UnitVec3::new_normalize(Vec3::new(0.0, 0.0, -1.0))
115        );
116    }
117
118    #[test]
119    fn test_raycast_triangle_from_below() {
120        let triangle = Triangle::new(
121            Point3::new(-1.0, 0.0, 0.0),
122            Point3::new(1.0, 0.0, 0.0),
123            Point3::new(0.0, 1.0, 0.0),
124        );
125
126        // Ray hitting from below
127        let ray = phys_geom::Ray::new(
128            Point3::new(0.0, 0.5, -2.0),
129            UnitVec3::new_normalize(Vec3::new(0.0, 0.0, 1.0)),
130        );
131
132        assert_eq!(triangle.raycast(ray, 10.0, false), None); // Ray hits backface
133    }
134
135    #[test]
136    fn test_raycast_triangle_miss() {
137        let triangle = Triangle::new(
138            Point3::new(-1.0, 0.0, 0.0),
139            Point3::new(1.0, 0.0, 0.0),
140            Point3::new(0.0, 1.0, 0.0),
141        );
142
143        // Ray that misses the triangle
144        let ray = phys_geom::Ray::new(
145            Point3::new(2.0, 2.0, 2.0),
146            UnitVec3::new_normalize(Vec3::new(0.0, 0.0, -1.0)),
147        );
148
149        assert_eq!(triangle.raycast(ray, 10.0, false), None);
150    }
151
152    #[test]
153    fn test_raycast_triangle_parallel() {
154        let triangle = Triangle::new(
155            Point3::new(-1.0, 0.0, 0.0),
156            Point3::new(1.0, 0.0, 0.0),
157            Point3::new(0.0, 1.0, 0.0),
158        );
159
160        // Ray parallel to the triangle
161        let ray = phys_geom::Ray::new(
162            Point3::new(0.0, 0.5, 2.0),
163            UnitVec3::new_normalize(Vec3::new(1.0, 0.0, 0.0)),
164        );
165
166        assert_eq!(triangle.raycast(ray, 10.0, false), None);
167    }
168
169    #[test]
170    fn test_raycast_triangle_max_distance() {
171        let triangle = Triangle::new(
172            Point3::new(-1.0, 0.0, 0.0),
173            Point3::new(1.0, 0.0, 0.0),
174            Point3::new(0.0, 1.0, 0.0),
175        );
176
177        let ray = phys_geom::Ray::new(
178            Point3::new(0.0, 0.5, 5.0),
179            UnitVec3::new_normalize(Vec3::new(0.0, 0.0, -1.0)),
180        );
181
182        // Max distance too short
183        assert_eq!(triangle.raycast(ray, 1.0, false), None);
184
185        // Max distance sufficient
186        let hit = triangle.raycast(ray, 10.0, false).unwrap();
187        assert_relative_eq!(hit.distance, 5.0);
188    }
189
190    #[test]
191    fn test_raycast_triangle_tilted() {
192        // Tilted triangle
193        let triangle = Triangle::new(
194            Point3::new(0.0, 0.0, 0.0),
195            Point3::new(1.0, 0.0, 1.0),
196            Point3::new(0.0, 1.0, 1.0),
197        );
198
199        // Ray hitting the tilted triangle
200        let ray = phys_geom::Ray::new(
201            Point3::new(0.2, 0.3, 2.0),
202            UnitVec3::new_normalize(Vec3::new(0.0, 0.0, -1.0)),
203        );
204
205        let hit = triangle.raycast(ray, 10.0, false).unwrap();
206        assert!(hit.distance > 0.0 && hit.distance < 2.0);
207
208        // Normal should point roughly upward
209        assert!(hit.normal.y > 0.0);
210        assert!(hit.normal.z < 0.0);
211    }
212
213    #[test]
214    fn test_raycast_triangle_edge_case() {
215        let triangle = Triangle::new(
216            Point3::new(0.0, 0.0, 0.0),
217            Point3::new(1.0, 0.0, 0.0),
218            Point3::new(0.0, 1.0, 0.0),
219        );
220
221        // Ray hitting right at the edge
222        let ray = phys_geom::Ray::new(
223            Point3::new(0.0, 0.0, 1.0),
224            UnitVec3::new_normalize(Vec3::new(0.0, 0.0, -1.0)),
225        );
226
227        let hit = triangle.raycast(ray, 10.0, false).unwrap();
228        assert_relative_eq!(hit.distance, 1.0);
229    }
230}