1use crate::vec::{Point3, Vec3};
10
11#[derive(Debug, Clone, Copy)]
13pub struct RayTriangleHit {
14 pub t: f64,
16 pub u: f64,
18 pub v: f64,
20}
21
22fn edge_tiebreak(px: f64, py: f64, qx: f64, qy: f64) -> f64 {
30 let a = px * qy;
34 let b = py * qx;
35 if a > b {
36 f64::MIN_POSITIVE
37 } else if a < b {
38 -f64::MIN_POSITIVE
39 } else {
40 if px > qx {
43 f64::MIN_POSITIVE
44 } else if px < qx {
45 -f64::MIN_POSITIVE
46 } else if py > qy {
47 f64::MIN_POSITIVE
48 } else {
49 -f64::MIN_POSITIVE
50 }
51 }
52}
53
54#[must_use]
67#[allow(clippy::many_single_char_names)]
68pub fn watertight_ray_triangle_intersect(
69 origin: Point3,
70 dir: Vec3,
71 v0: Point3,
72 v1: Point3,
73 v2: Point3,
74) -> Option<RayTriangleHit> {
75 let d = dir.0;
76
77 let abs_x = d[0].abs();
79 let abs_y = d[1].abs();
80 let abs_z = d[2].abs();
81
82 let kz = if abs_x > abs_y && abs_x > abs_z {
83 0
84 } else if abs_y > abs_z {
85 1
86 } else {
87 2
88 };
89 let mut kx = (kz + 1) % 3;
90 let mut ky = (kx + 1) % 3;
91
92 if d[kz] < 0.0 {
94 std::mem::swap(&mut kx, &mut ky);
95 }
96
97 let sz = 1.0 / d[kz];
99 let sx = d[kx] * sz;
100 let sy = d[ky] * sz;
101
102 let a = (v0 - origin).0;
104 let b = (v1 - origin).0;
105 let c = (v2 - origin).0;
106
107 let ax = a[kx] - sx * a[kz];
109 let ay = a[ky] - sy * a[kz];
110 let bx = b[kx] - sx * b[kz];
111 let by = b[ky] - sy * b[kz];
112 let cx = c[kx] - sx * c[kz];
113 let cy = c[ky] - sy * c[kz];
114
115 let mut u = cx.mul_add(by, -(cy * bx));
117 let mut v = ax.mul_add(cy, -(ay * cx));
118 let mut w = bx.mul_add(ay, -(by * ax));
119
120 if u == 0.0 {
125 u = edge_tiebreak(cx, cy, bx, by);
126 }
127 if v == 0.0 {
128 v = edge_tiebreak(ax, ay, cx, cy);
129 }
130 if w == 0.0 {
131 w = edge_tiebreak(bx, by, ax, ay);
132 }
133
134 if (u < 0.0 || v < 0.0 || w < 0.0) && (u > 0.0 || v > 0.0 || w > 0.0) {
136 return None;
137 }
138
139 let det = u + v + w;
141 if det == 0.0 {
142 return None;
143 }
144
145 let az = sz * a[kz];
147 let bz = sz * b[kz];
148 let cz = sz * c[kz];
149 let t_scaled = u.mul_add(az, v.mul_add(bz, w * cz));
150
151 if (det > 0.0 && t_scaled <= 0.0) || (det < 0.0 && t_scaled >= 0.0) {
154 return None;
155 }
156
157 let inv_det = 1.0 / det;
159 Some(RayTriangleHit {
160 t: t_scaled * inv_det,
161 u: v * inv_det,
162 v: w * inv_det,
163 })
164}
165
166#[cfg(test)]
167#[allow(
168 clippy::unwrap_used,
169 clippy::expect_used,
170 clippy::cast_lossless,
171 clippy::suboptimal_flops
172)]
173mod tests {
174 use super::*;
175
176 const EPS: f64 = 1e-12;
177
178 fn tri() -> (Point3, Point3, Point3) {
179 (
180 Point3::new(-1.0, -1.0, 0.0),
181 Point3::new(1.0, -1.0, 0.0),
182 Point3::new(0.0, 1.0, 0.0),
183 )
184 }
185
186 #[test]
187 fn ray_hits_triangle() {
188 let (v0, v1, v2) = tri();
189 let origin = Point3::new(0.0, 0.0, -1.0);
190 let dir = Vec3::new(0.0, 0.0, 1.0);
191
192 let hit = watertight_ray_triangle_intersect(origin, dir, v0, v1, v2).expect("should hit");
193 assert!((hit.t - 1.0).abs() < EPS);
194 }
195
196 #[test]
197 fn ray_misses_triangle() {
198 let (v0, v1, v2) = tri();
199 let origin = Point3::new(10.0, 10.0, -1.0);
200 let dir = Vec3::new(0.0, 0.0, 1.0);
201
202 assert!(watertight_ray_triangle_intersect(origin, dir, v0, v1, v2).is_none());
203 }
204
205 #[test]
206 fn ray_parallel_to_triangle() {
207 let (v0, v1, v2) = tri();
208 let origin = Point3::new(0.0, 0.0, 0.0);
209 let dir = Vec3::new(1.0, 0.0, 0.0);
210
211 assert!(watertight_ray_triangle_intersect(origin, dir, v0, v1, v2).is_none());
212 }
213
214 #[test]
215 fn shared_edge_exactly_one_hit() {
216 let shared_a = Point3::new(0.0, 0.0, 0.0);
218 let shared_b = Point3::new(1.0, 0.0, 0.0);
219 let tri1_c = Point3::new(0.5, 1.0, 0.0);
220 let tri2_c = Point3::new(0.5, -1.0, 0.0);
221
222 let origin = Point3::new(0.5, 0.0, -1.0);
224 let dir = Vec3::new(0.0, 0.0, 1.0);
225
226 let hit1 = watertight_ray_triangle_intersect(origin, dir, shared_a, shared_b, tri1_c);
227 let hit2 = watertight_ray_triangle_intersect(origin, dir, shared_a, shared_b, tri2_c);
228
229 let count = hit1.is_some() as u32 + hit2.is_some() as u32;
230 assert_eq!(count, 1, "shared edge must report exactly one hit");
231 }
232
233 #[test]
234 fn ray_hits_vertex() {
235 let shared = Point3::new(0.0, 0.0, 0.0);
238 let tris = [
239 (
240 shared,
241 Point3::new(1.0, 0.0, 0.0),
242 Point3::new(0.0, 1.0, 0.0),
243 ),
244 (
245 shared,
246 Point3::new(0.0, 1.0, 0.0),
247 Point3::new(-1.0, 0.0, 0.0),
248 ),
249 (
250 shared,
251 Point3::new(-1.0, 0.0, 0.0),
252 Point3::new(0.0, -1.0, 0.0),
253 ),
254 (
255 shared,
256 Point3::new(0.0, -1.0, 0.0),
257 Point3::new(1.0, 0.0, 0.0),
258 ),
259 ];
260
261 let origin = Point3::new(0.0, 0.0, -1.0);
262 let dir = Vec3::new(0.0, 0.0, 1.0);
263
264 let count: u32 = tris
265 .iter()
266 .map(|&(a, b, c)| {
267 watertight_ray_triangle_intersect(origin, dir, a, b, c).is_some() as u32
268 })
269 .sum();
270
271 assert_eq!(
272 count, 1,
273 "vertex shared by 4 triangles must report exactly one hit"
274 );
275 }
276
277 #[test]
278 fn backface_not_hit() {
279 let (v0, v1, v2) = tri();
280 let origin = Point3::new(0.0, 0.0, -1.0);
282 let dir = Vec3::new(0.0, 0.0, -1.0);
283
284 assert!(watertight_ray_triangle_intersect(origin, dir, v0, v1, v2).is_none());
285 }
286
287 #[test]
288 fn barycentric_coordinates_valid() {
289 let (v0, v1, v2) = tri();
290 let origin = Point3::new(0.0, 0.0, -1.0);
291 let dir = Vec3::new(0.0, 0.0, 1.0);
292
293 let hit = watertight_ray_triangle_intersect(origin, dir, v0, v1, v2).expect("should hit");
294
295 assert!(hit.u >= -EPS, "u = {} should be >= 0", hit.u);
296 assert!(hit.v >= -EPS, "v = {} should be >= 0", hit.v);
297 assert!(
298 hit.u + hit.v <= 1.0 + EPS,
299 "u + v = {} should be <= 1",
300 hit.u + hit.v,
301 );
302
303 let w = 1.0 - hit.u - hit.v;
305 let px = w * v0.x() + hit.u * v1.x() + hit.v * v2.x();
306 let py = w * v0.y() + hit.u * v1.y() + hit.v * v2.y();
307 let pz = w * v0.z() + hit.u * v1.z() + hit.v * v2.z();
308
309 let expected = origin.0[2] + hit.t * dir.0[2];
310 assert!(
311 (pz - expected).abs() < EPS,
312 "pz = {pz}, expected = {expected}"
313 );
314 assert!((px - origin.x()).abs() < EPS);
316 assert!((py - origin.y()).abs() < EPS);
317 }
318}