1use crate::frame::Frame3;
4use crate::nurbs::decompose::surface_to_bezier_patches;
5use crate::nurbs::surface::NurbsSurface;
6use crate::vec::{Point3, Vec3};
7
8const MAX_DEPTH: u32 = 48;
10
11const MAX_VISITS: usize = 20_000;
13
14const LEAF: f64 = 1e-3;
17
18const GRAZE_COS: f64 = 1e-9;
20
21const SEPARATION: f64 = 4.0;
24
25#[derive(Debug, Clone)]
32pub struct RaySurface {
33 surface: NurbsSurface,
34 patches: Vec<Patch>,
35 scale: f64,
36}
37
38#[derive(Debug, Clone)]
39struct Patch {
40 rows: usize,
41 cols: usize,
42 pts: Vec<[f64; 4]>,
45 u: (f64, f64),
46 v: (f64, f64),
47}
48
49#[derive(Debug, Clone, Copy)]
51pub struct RayHit {
52 pub u: f64,
54 pub v: f64,
56 pub t: f64,
58 pub point: Point3,
60}
61
62#[derive(Debug, Clone, Default)]
65pub struct RayHits {
66 pub hits: Vec<RayHit>,
68 pub grazed: bool,
71}
72
73impl Patch {
74 fn point(&self, i: usize, j: usize) -> Point3 {
75 let [x, y, z, w] = self.pts[i * self.cols + j];
76 Point3::new(x / w, y / w, z / w)
77 }
78
79 fn split(&self, along_u: bool) -> (Self, Self) {
82 let (n, m) = if along_u {
83 (self.rows, self.cols)
84 } else {
85 (self.cols, self.rows)
86 };
87 let at = |k: usize, l: usize| {
88 if along_u {
89 k * self.cols + l
90 } else {
91 l * self.cols + k
92 }
93 };
94 let mut low = self.pts.clone();
95 let mut high = self.pts.clone();
96 let mut work = vec![[0.0; 4]; n];
97 for l in 0..m {
98 for (k, slot) in work.iter_mut().enumerate() {
99 *slot = self.pts[at(k, l)];
100 }
101 low[at(0, l)] = work[0];
102 high[at(n - 1, l)] = work[n - 1];
103 for r in 1..n {
104 for k in 0..n - r {
105 for c in 0..4 {
106 work[k][c] = 0.5 * (work[k][c] + work[k + 1][c]);
107 }
108 }
109 low[at(r, l)] = work[0];
110 high[at(n - 1 - r, l)] = work[n - 1 - r];
111 }
112 }
113 let (mut a, mut b) = (self.clone(), self.clone());
114 a.pts = low;
115 b.pts = high;
116 if along_u {
117 let mid = f64::midpoint(self.u.0, self.u.1);
118 (a.u, b.u) = ((self.u.0, mid), (mid, self.u.1));
119 } else {
120 let mid = f64::midpoint(self.v.0, self.v.1);
121 (a.v, b.v) = ((self.v.0, mid), (mid, self.v.1));
122 }
123 (a, b)
124 }
125
126 fn spans(&self) -> (f64, f64) {
130 let mut along_u = 0.0_f64;
131 for j in 0..self.cols {
132 let length: f64 = (1..self.rows)
133 .map(|i| (self.point(i, j) - self.point(i - 1, j)).length())
134 .sum();
135 along_u = along_u.max(length);
136 }
137 let mut along_v = 0.0_f64;
138 for i in 0..self.rows {
139 let length: f64 = (1..self.cols)
140 .map(|j| (self.point(i, j) - self.point(i, j - 1)).length())
141 .sum();
142 along_v = along_v.max(length);
143 }
144 (along_u, along_v)
145 }
146}
147
148impl RaySurface {
149 #[must_use]
153 pub fn new(surface: &NurbsSurface) -> Option<Self> {
154 let clamped = |knots: &[f64], degree: usize| {
157 let n = knots.len();
158 n > 2 * degree + 1
159 && knots[..=degree]
160 .iter()
161 .all(|&k| (k - knots[0]).abs() <= 0.0)
162 && knots[n - degree - 1..]
163 .iter()
164 .all(|&k| (k - knots[n - 1]).abs() <= 0.0)
165 };
166 if surface
167 .weights()
168 .iter()
169 .flatten()
170 .any(|&w| !w.is_finite() || w <= 0.0)
171 || !clamped(surface.knots_u(), surface.degree_u())
172 || !clamped(surface.knots_v(), surface.degree_v())
173 {
174 return None;
175 }
176 let patches = surface_to_bezier_patches(surface)
177 .ok()?
178 .into_iter()
179 .map(|p| {
180 let cps = p.surface.control_points();
181 let ws = p.surface.weights();
182 let rows = cps.len();
183 let cols = cps.first().map_or(0, Vec::len);
184 let pts = cps
185 .iter()
186 .zip(ws)
187 .flat_map(|(row, wrow)| {
188 row.iter()
189 .zip(wrow)
190 .map(|(c, &w)| [c.x() * w, c.y() * w, c.z() * w, w])
191 })
192 .collect();
193 Patch {
194 rows,
195 cols,
196 pts,
197 u: p.u_range,
198 v: p.v_range,
199 }
200 })
201 .filter(|p| p.rows > 0 && p.cols > 0)
202 .collect();
203 let b = surface.aabb();
204 let scale = (b.max - b.min).length().max(1e-9);
205 Some(Self {
206 surface: surface.clone(),
207 patches,
208 scale,
209 })
210 }
211
212 #[must_use]
214 pub const fn surface(&self) -> &NurbsSurface {
215 &self.surface
216 }
217
218 #[must_use]
223 pub fn hits(&self, origin: Point3, dir: Vec3, reach: f64) -> RayHits {
224 let mut out = RayHits::default();
225 let mut radii: Vec<f64> = Vec::new();
228 out = self.collect_hits(origin, dir, reach, (out, &mut radii));
229 coalesce(&mut out.hits, &radii);
230 out
231 }
232
233 fn collect_hits(
236 &self,
237 origin: Point3,
238 dir: Vec3,
239 reach: f64,
240 (mut out, radii): (RayHits, &mut Vec<f64>),
241 ) -> RayHits {
242 let Ok(frame) = Frame3::from_normal(origin, dir) else {
243 out.grazed = true;
244 return out;
245 };
246 let leaf = LEAF * self.scale;
247 let mut stack: Vec<(Patch, u32)> = self.patches.iter().map(|p| (p.clone(), 0)).collect();
248 let mut visits = 0_usize;
249 while let Some((patch, depth)) = stack.pop() {
250 visits += 1;
251 if visits > MAX_VISITS {
252 out.grazed = true;
253 return out;
254 }
255 let (mut lo, mut hi) = ([f64::INFINITY; 3], [f64::NEG_INFINITY; 3]);
256 for i in 0..patch.rows {
257 for j in 0..patch.cols {
258 let d = patch.point(i, j) - origin;
259 for (k, x) in [frame.x.dot(d), frame.y.dot(d), frame.z.dot(d)]
260 .into_iter()
261 .enumerate()
262 {
263 lo[k] = lo[k].min(x);
264 hi[k] = hi[k].max(x);
265 }
266 }
267 }
268 if lo[0] > reach || hi[0] < -reach || lo[1] > reach || hi[1] < -reach || hi[2] < -reach
269 {
270 continue;
271 }
272 let (along_u, along_v) = patch.spans();
273 if along_u.max(along_v) <= leaf || depth >= MAX_DEPTH {
274 if self.solve(&patch, origin, frame.z, reach, (&mut out, radii)) {
277 continue;
278 }
279 if depth >= MAX_DEPTH {
280 out.grazed = true;
281 continue;
282 }
283 }
284 let (a, b) = patch.split(along_u >= along_v);
285 stack.push((a, depth + 1));
286 stack.push((b, depth + 1));
287 }
288 out
289 }
290
291 fn second_crossing_near(&self, u: f64, v: f64, dir: Vec3) -> bool {
297 let d = self.surface.derivatives(u, v, 2);
298 let (su, sv) = (d[1][0], d[0][1]);
299 let Ok(n) = su.cross(sv).normalize() else {
300 return true;
301 };
302 let slant = n.dot(dir);
303 if slant.abs() <= GRAZE_COS {
304 return true;
305 }
306 let w = dir - n * slant;
307 let (guu, guv, gvv) = (su.dot(su), su.dot(sv), sv.dot(sv));
308 let det = guu.mul_add(gvv, -guv * guv);
309 if det <= 0.0 || w.length() <= GRAZE_COS {
310 return false;
311 }
312 let (wu, wv) = (su.dot(w), sv.dot(w));
313 let a = gvv.mul_add(wu, -guv * wv) / det;
314 let b = guu.mul_add(wv, -guv * wu) / det;
315 let bend = n.dot(d[2][0] * (a * a) + d[1][1] * (2.0 * a * b) + d[0][2] * (b * b));
316 2.0 * slant.abs() <= SEPARATION * LEAF * self.scale * bend.abs()
317 }
318
319 fn solve(
331 &self,
332 leaf: &Patch,
333 origin: Point3,
334 dir: Vec3,
335 reach: f64,
336 (out, radii): (&mut RayHits, &mut Vec<f64>),
337 ) -> bool {
338 let (mut u, mut v) = (
339 f64::midpoint(leaf.u.0, leaf.u.1),
340 f64::midpoint(leaf.v.0, leaf.v.1),
341 );
342 let (du, dv) = (self.surface.domain_u(), self.surface.domain_v());
343 let mut t = dir.dot(self.surface.evaluate(u, v) - origin);
344 let mut stalled = None;
345 for _ in 0..32 {
346 let d = self.surface.derivatives(u, v, 1);
347 let (s, su, sv) = (d[0][0], d[1][0], d[0][1]);
348 let f = s - Vec3::new(origin.x(), origin.y(), origin.z()) - dir * t;
349 if f.length() <= 1e-12 * self.scale {
350 return self.record(leaf, (u, v, t), origin, dir, reach, reach, (out, radii));
351 }
352 let spacing = 4.0 * f64::EPSILON * su.length().mul_add(u.abs(), sv.length() * v.abs());
353 stalled = (f.length() <= spacing).then_some((u, v, t, spacing));
354 let normal = su.cross(sv);
355 let det = -normal.dot(dir);
356 if det.abs() <= 1e-300 {
357 break;
358 }
359 let neg_dir = -dir;
361 let rhs = -f;
362 let x_u = rhs.cross(sv).dot(neg_dir) / det;
363 let x_v = su.cross(rhs).dot(neg_dir) / det;
364 let x_t = normal.dot(rhs) / det;
365 u = (u + x_u).clamp(du.0, du.1);
366 v = (v + x_v).clamp(dv.0, dv.1);
367 t += x_t;
368 }
369 let Some((u, v, t, spacing)) = stalled else {
370 return false;
371 };
372 out.grazed |= spacing > reach;
373 self.record(
374 leaf,
375 (u, v, t),
376 origin,
377 dir,
378 reach,
379 spacing.max(reach),
380 (out, radii),
381 )
382 }
383
384 #[allow(clippy::too_many_arguments)]
389 fn record(
390 &self,
391 leaf: &Patch,
392 (u, v, t): (f64, f64, f64),
393 origin: Point3,
394 dir: Vec3,
395 reach: f64,
396 merge: f64,
397 (out, radii): (&mut RayHits, &mut Vec<f64>),
398 ) -> bool {
399 let (du, dv) = (self.surface.domain_u(), self.surface.domain_v());
400 let slack_u = 1e-9 * (du.1 - du.0);
401 let slack_v = 1e-9 * (dv.1 - dv.0);
402 if u < leaf.u.0 - slack_u
403 || u > leaf.u.1 + slack_u
404 || v < leaf.v.0 - slack_v
405 || v > leaf.v.1 + slack_v
406 || t < -reach
407 {
408 return true;
409 }
410 out.grazed |= self.second_crossing_near(u, v, dir);
411 let point = origin + dir * t;
412 out.hits.push(RayHit { u, v, t, point });
413 radii.push(merge);
414 true
415 }
416}
417
418fn coalesce(hits: &mut Vec<RayHit>, radii: &[f64]) {
431 struct Crossing {
434 last: f64,
435 reacher: (f64, f64),
436 best: usize,
437 }
438 let farther = |a: (f64, f64), b: (f64, f64)| a.0 - b.0 > b.1 - a.1;
443 let mut order: Vec<usize> = (0..hits.len()).collect();
444 order.sort_by(|&a, &b| hits[a].t.total_cmp(&hits[b].t));
445 let precise = |a: usize, b: usize| {
446 radii[a]
447 .total_cmp(&radii[b])
448 .then(hits[a].t.total_cmp(&hits[b].t))
449 .is_lt()
450 };
451 let mut crossings: Vec<Crossing> = Vec::new();
452 for i in order {
453 let (t, r) = (hits[i].t, radii[i]);
454 match crossings.last_mut() {
455 Some(c) if t - c.reacher.0 <= c.reacher.1 || t - c.last <= r => {
456 c.last = t;
457 if farther((t, r), c.reacher) {
458 c.reacher = (t, r);
459 }
460 if precise(i, c.best) {
461 c.best = i;
462 }
463 }
464 _ => crossings.push(Crossing {
465 last: t,
466 reacher: (t, r),
467 best: i,
468 }),
469 }
470 while crossings.len() >= 2 && t - crossings[crossings.len() - 2].last <= r {
471 let Some(later) = crossings.pop() else { break };
472 if let Some(c) = crossings.last_mut() {
473 c.last = later.last;
474 if farther(later.reacher, c.reacher) {
475 c.reacher = later.reacher;
476 }
477 if precise(later.best, c.best) {
478 c.best = later.best;
479 }
480 }
481 }
482 }
483 *hits = crossings.into_iter().map(|c| hits[c.best]).collect();
484}
485
486#[cfg(test)]
487mod tests {
488 #![allow(clippy::unwrap_used)]
489
490 use super::*;
491
492 fn lid() -> NurbsSurface {
495 let rows = (0..6)
496 .map(|i| {
497 (0..3)
498 .map(|j| {
499 let z = if (i, j) == (1, 1) { 10.0 } else { 0.0 };
500 Point3::new(f64::from(i) / 5.0, f64::from(j) / 2.0, z)
501 })
502 .collect()
503 })
504 .collect();
505 NurbsSurface::new(
506 5,
507 2,
508 [vec![0.0; 6], vec![1.0; 6]].concat(),
509 vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
510 rows,
511 vec![vec![1.0; 3]; 6],
512 )
513 .unwrap()
514 }
515
516 #[test]
517 fn a_ray_up_through_the_lids_peak_meets_it_there() {
518 let lid = RaySurface::new(&lid()).unwrap();
519 let up = Vec3::new(0.0, 0.0, 1.0);
520 let at_peak = lid.hits(Point3::new(0.2, 0.5, -1.0), up, 1e-9);
521 assert!(!at_peak.grazed);
522 assert_eq!(at_peak.hits.len(), 1);
523 assert!((at_peak.hits[0].point.z() - 2.048).abs() < 1e-12);
524 assert!((at_peak.hits[0].t - 3.048).abs() < 1e-12);
525 assert!((at_peak.hits[0].u - 0.2).abs() < 1e-9 && (at_peak.hits[0].v - 0.5).abs() < 1e-9);
526
527 let beside = lid.hits(Point3::new(1.5, 0.5, -1.0), up, 1e-9);
528 assert!(beside.hits.is_empty() && !beside.grazed);
529 let behind = lid.hits(Point3::new(0.2, 0.5, 3.0), up, 1e-9);
530 assert!(behind.hits.is_empty());
531 }
532
533 #[test]
536 fn a_level_ray_crosses_the_bulge_twice() {
537 let lid = RaySurface::new(&lid()).unwrap();
538 let across = lid.hits(Point3::new(-1.0, 0.5, 1.5), Vec3::new(1.0, 0.0, 0.0), 1e-9);
539 assert!(!across.grazed);
540 assert_eq!(across.hits.len(), 2, "{:?}", across.hits);
541 for h in &across.hits {
542 let u = h.point.x();
543 assert!((25.0 * u * (1.0 - u).powi(4) - 1.5).abs() < 1e-9, "{h:?}");
544 }
545 }
546
547 #[test]
548 fn a_ray_touching_the_peak_grazes() {
549 let lid = RaySurface::new(&lid()).unwrap();
550 let level = lid.hits(
551 Point3::new(-1.0, 0.5, 2.048),
552 Vec3::new(1.0, 0.0, 0.0),
553 1e-9,
554 );
555 assert!(level.grazed, "{level:?}");
556 }
557
558 #[test]
563 fn a_level_ray_crosses_a_tall_spike() {
564 let rows = (0..3)
565 .map(|i| {
566 (0..3)
567 .map(|j| {
568 let z = if (i, j) == (1, 1) { 4000.0 } else { 0.0 };
569 Point3::new(f64::from(i) / 2.0, f64::from(j) / 2.0, z)
570 })
571 .collect()
572 })
573 .collect();
574 let knots = vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0];
575 let spike =
576 NurbsSurface::new(2, 2, knots.clone(), knots, rows, vec![vec![1.0; 3]; 3]).unwrap();
577 let ray = RaySurface::new(&spike).unwrap();
578 let across = ray.hits(
579 Point3::new(-1.0, 0.5, 500.0),
580 Vec3::new(1.0, 0.0, 0.0),
581 1e-9,
582 );
583 assert!(!across.grazed);
584 let mut xs: Vec<f64> = across.hits.iter().map(|h| h.point.x()).collect();
585 xs.sort_by(f64::total_cmp);
586 let root = std::f64::consts::FRAC_1_SQRT_2;
587 assert_eq!(xs.len(), 2, "{xs:?}");
588 assert!((xs[0] - (1.0 - root) / 2.0).abs() < 1e-9, "{xs:?}");
589 assert!((xs[1] - f64::midpoint(1.0, root)).abs() < 1e-9, "{xs:?}");
590 }
591
592 #[test]
598 fn a_ray_level_with_a_saddles_middle_crosses_it() {
599 let p = Point3::new;
600 let rows = vec![
601 vec![p(0.0, 0.0, 0.25), p(0.0, 1.0, -0.25)],
602 vec![p(1.0, 0.0, -0.25), p(1.0, 1.0, 0.25)],
603 vec![p(2000.0, 0.0, 0.0), p(2000.0, 1.0, 0.0)],
604 ];
605 let s = NurbsSurface::new(
606 1,
607 1,
608 vec![0.0, 0.0, 1.0, 2.0, 2.0],
609 vec![0.0, 0.0, 1.0, 1.0],
610 rows,
611 vec![vec![1.0; 2]; 3],
612 )
613 .unwrap();
614 let ray = RaySurface::new(&s).unwrap();
615 let dir = Vec3::new(1.0, 1.0, 0.0).normalize().unwrap();
616 let found = ray.hits(p(0.5, 0.5, 0.04), dir, 1e-9);
617 assert_eq!(found.hits.len(), 1, "{:?}", found.grazed);
618 assert!((found.hits[0].point - p(0.7, 0.7, 0.04)).length() < 1e-9);
619 }
620
621 #[test]
624 fn an_unclamped_surface_is_not_ready() {
625 let rows = (0..3)
626 .map(|i| {
627 (0..2)
628 .map(|j| Point3::new(0.5 * f64::from(i), f64::from(j), 0.0))
629 .collect()
630 })
631 .collect();
632 let s = NurbsSurface::new(
633 2,
634 1,
635 vec![-2.0, -1.0, 0.0, 1.0, 2.0, 3.0],
636 vec![0.0, 0.0, 1.0, 1.0],
637 rows,
638 vec![vec![1.0; 2]; 3],
639 )
640 .unwrap();
641 assert!(RaySurface::new(&s).is_none());
642 }
643
644 #[test]
645 fn a_surface_with_a_negative_weight_is_not_ready() {
646 let mut weights = vec![vec![1.0; 3]; 6];
647 weights[2][1] = -1.0;
648 let s = lid();
649 let s = NurbsSurface::new(
650 5,
651 2,
652 s.knots_u().to_vec(),
653 s.knots_v().to_vec(),
654 s.control_points().to_vec(),
655 weights,
656 )
657 .unwrap();
658 assert!(RaySurface::new(&s).is_none());
659 }
660
661 #[test]
668 fn a_patch_on_a_tiny_or_far_knot_domain_is_crossed_once() {
669 let p = Point3::new;
670 let corners = vec![
671 vec![p(0.0, 0.0, 0.0), p(0.0, 1.0, 0.0)],
672 vec![p(1.0, 0.0, 0.0), p(1.0, 1.0, 0.0)],
673 ];
674 let dir = Vec3::new(0.3, 0.2, -1.0);
675 let far = 2.0_f64.powi(40);
676 let middle = p(0.5, 0.5, 0.0);
677 for (lo, hi, through, flagged) in [
678 (0.0, 1.0, middle, false),
679 (0.0, 1e-12, middle, false),
680 (far, far + 1.0, middle, false),
681 (far, far + 1.0, p(0.500_03, 0.500_07, 0.0), true),
682 ] {
683 let knots = vec![lo, lo, hi, hi];
684 let s = NurbsSurface::new(
685 1,
686 1,
687 knots.clone(),
688 knots,
689 corners.clone(),
690 vec![vec![1.0; 2]; 2],
691 )
692 .unwrap();
693 let found = RaySurface::new(&s).unwrap().hits(through - dir, dir, 1e-6);
694 let case = format!("knots {lo} to {hi} through {through:?}");
695 assert_eq!(found.hits.len(), 1, "{case}: {:?}", found.hits);
696 assert_eq!(found.grazed, flagged, "{case}");
697 }
698 }
699
700 #[test]
706 fn hits_merge_on_the_wider_radius_in_any_order() {
707 let p = Point3::new;
708 let knots = vec![0.0, 0.0, 1.0, 1.0];
709 let flat = NurbsSurface::new(
710 1,
711 1,
712 knots.clone(),
713 knots,
714 vec![
715 vec![p(0.0, 0.0, 0.0), p(0.0, 1.0, 0.0)],
716 vec![p(1.0, 0.0, 0.0), p(1.0, 1.0, 0.0)],
717 ],
718 vec![vec![1.0; 2]; 2],
719 )
720 .unwrap();
721 let ray = RaySurface::new(&flat).unwrap();
722 let leaf = ray.patches[0].clone();
723 let (origin, dir) = (p(0.5, 0.5, 1.0), Vec3::new(0.0, 0.0, -1.0));
724 let chains = [
728 [
729 ((0.5, 0.5, 1.0), 1e-6),
730 ((0.5, 0.5, 1.0001), 1e-3),
731 ((0.5, 0.5, 1.0002), 1e-6),
732 ],
733 [
734 ((0.5, 0.5, 1.0), 1e-3),
735 ((0.5, 0.5, 1.002), 1e-6),
736 ((0.5, 0.5, 1.0021), 3e-3),
737 ],
738 ];
739 let orders = [
740 [0, 1, 2],
741 [0, 2, 1],
742 [1, 0, 2],
743 [1, 2, 0],
744 [2, 0, 1],
745 [2, 1, 0],
746 ];
747 let far = 1e10_f64;
750 let next = f64::from_bits(far.to_bits() + 1);
751 for order in [[0, 1], [1, 0]] {
752 let (mut out, mut radii) = (RayHits::default(), Vec::new());
753 for k in order {
754 let t = [far, next][k];
755 assert!(ray.record(
756 &leaf,
757 (0.5, 0.5, t),
758 origin,
759 dir,
760 1e-6,
761 1e-6,
762 (&mut out, &mut radii)
763 ));
764 }
765 coalesce(&mut out.hits, &radii);
766 assert_eq!(out.hits.len(), 2, "far along the ray in {order:?}");
767 }
768 for found in chains {
769 for order in orders {
770 let (mut out, mut radii) = (RayHits::default(), Vec::new());
771 for k in order {
772 let (hit, merge) = found[k];
773 assert!(ray.record(
774 &leaf,
775 hit,
776 origin,
777 dir,
778 1e-6,
779 merge,
780 (&mut out, &mut radii)
781 ));
782 }
783 coalesce(&mut out.hits, &radii);
784 assert_eq!(out.hits.len(), 1, "{found:?} in {order:?}: {:?}", out.hits);
785 }
786 }
787 }
788}