Skip to main content

brepkit_math/nurbs/intersection/
ray.rs

1//! Every crossing of a ray with a NURBS surface.
2
3use crate::frame::Frame3;
4use crate::nurbs::decompose::surface_to_bezier_patches;
5use crate::nurbs::surface::NurbsSurface;
6use crate::vec::{Point3, Vec3};
7
8/// Deepest subdivision of one Bezier patch.
9const MAX_DEPTH: u32 = 48;
10
11/// Most patches one ray may visit before it is reported as grazing.
12const MAX_VISITS: usize = 20_000;
13
14/// A leaf's size against the surface's: below it, Newton from the leaf's
15/// middle finds the crossing the leaf holds.
16const LEAF: f64 = 1e-3;
17
18/// The least slant a hit is read at, however flat the surface.
19const GRAZE_COS: f64 = 1e-9;
20
21/// How many leaves apart a hit's second crossing may lie and still share a
22/// leaf's Newton with it.
23const SEPARATION: f64 = 4.0;
24
25/// A NURBS surface made ready for ray crossings.
26///
27/// It holds the surface's Bezier patches, each a grid of homogeneous control
28/// points. A surface whose weights are all positive lies in the hull of each
29/// patch's control points, which the search relies on to pass over the
30/// patches a ray misses.
31#[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    /// `rows` rows of `cols` homogeneous points `[w x, w y, w z, w]`, a row
43    /// per `u` index.
44    pts: Vec<[f64; 4]>,
45    u: (f64, f64),
46    v: (f64, f64),
47}
48
49/// Where a ray meets a surface.
50#[derive(Debug, Clone, Copy)]
51pub struct RayHit {
52    /// The surface's `u` there.
53    pub u: f64,
54    /// The surface's `v` there.
55    pub v: f64,
56    /// The distance along the ray's unit direction.
57    pub t: f64,
58    /// The point.
59    pub point: Point3,
60}
61
62/// A ray's hits on a surface, and whether any of them, or a place the ray
63/// passes, is too near tangent for the count of hits to be trusted.
64#[derive(Debug, Clone, Default)]
65pub struct RayHits {
66    /// The hits, each once.
67    pub hits: Vec<RayHit>,
68    /// The ray touches the surface tangentially, runs in it, or passes
69    /// within the reach of it without a crossing that settles.
70    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    /// The patch split at the middle of its `u` range (`along_u`) or its
80    /// `v` range, by de Casteljau on the homogeneous points.
81    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    /// The longest control polygon along `u` and along `v`: each bounds the
127    /// length of the patch's curves that way, however far a bulge between
128    /// its ends rises.
129    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    /// `surface` made ready, or `None` when a weight is not positive, a knot
150    /// vector is not clamped, or the surface cannot be cut into Bezier
151    /// patches.
152    #[must_use]
153    pub fn new(surface: &NurbsSurface) -> Option<Self> {
154        // An unclamped knot vector's end spans are not Bezier patches of the
155        // surface as decomposed, so the search would cross a different one.
156        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    /// The surface.
213    #[must_use]
214    pub const fn surface(&self) -> &NurbsSurface {
215        &self.surface
216    }
217
218    /// Every point where the line through `origin` along `dir` meets the
219    /// surface no farther than `reach` behind `origin`, with `t` measured
220    /// along `dir` made unit. A hit within `reach` of another is the same
221    /// hit.
222    #[must_use]
223    pub fn hits(&self, origin: Point3, dir: Vec3, reach: f64) -> RayHits {
224        let mut out = RayHits::default();
225        let Ok(frame) = Frame3::from_normal(origin, dir) else {
226            out.grazed = true;
227            return out;
228        };
229        let leaf = LEAF * self.scale;
230        let mut stack: Vec<(Patch, u32)> = self.patches.iter().map(|p| (p.clone(), 0)).collect();
231        let mut visits = 0_usize;
232        while let Some((patch, depth)) = stack.pop() {
233            visits += 1;
234            if visits > MAX_VISITS {
235                out.grazed = true;
236                return out;
237            }
238            let (mut lo, mut hi) = ([f64::INFINITY; 3], [f64::NEG_INFINITY; 3]);
239            for i in 0..patch.rows {
240                for j in 0..patch.cols {
241                    let d = patch.point(i, j) - origin;
242                    for (k, x) in [frame.x.dot(d), frame.y.dot(d), frame.z.dot(d)]
243                        .into_iter()
244                        .enumerate()
245                    {
246                        lo[k] = lo[k].min(x);
247                        hi[k] = hi[k].max(x);
248                    }
249                }
250            }
251            if lo[0] > reach || hi[0] < -reach || lo[1] > reach || hi[1] < -reach || hi[2] < -reach
252            {
253                continue;
254            }
255            let (along_u, along_v) = patch.spans();
256            if along_u.max(along_v) <= leaf || depth >= MAX_DEPTH {
257                // A leaf the solve cannot settle is cut further, and at the
258                // deepest level the ray is taken to graze it.
259                if self.solve(&patch, origin, frame.z, reach, &mut out) {
260                    continue;
261                }
262                if depth >= MAX_DEPTH {
263                    out.grazed = true;
264                    continue;
265                }
266            }
267            let (a, b) = patch.split(along_u >= along_v);
268            stack.push((a, depth + 1));
269            stack.push((b, depth + 1));
270        }
271        out
272    }
273
274    /// Whether a ray along `dir` through the surface at `(u, v)` meets it
275    /// again too near to be found apart: bending by its normal curvature
276    /// `k` along the ray's tangential part `w`, the surface comes back to
277    /// the ray `2 (n . dir) / (k |w|^2)` along it. A ray at a tangent point
278    /// meets it there twice.
279    fn second_crossing_near(&self, u: f64, v: f64, dir: Vec3) -> bool {
280        let d = self.surface.derivatives(u, v, 2);
281        let (su, sv) = (d[1][0], d[0][1]);
282        let Ok(n) = su.cross(sv).normalize() else {
283            return true;
284        };
285        let slant = n.dot(dir);
286        if slant.abs() <= GRAZE_COS {
287            return true;
288        }
289        let w = dir - n * slant;
290        let (guu, guv, gvv) = (su.dot(su), su.dot(sv), sv.dot(sv));
291        let det = guu.mul_add(gvv, -guv * guv);
292        if det <= 0.0 || w.length() <= GRAZE_COS {
293            return false;
294        }
295        let (wu, wv) = (su.dot(w), sv.dot(w));
296        let a = gvv.mul_add(wu, -guv * wv) / det;
297        let b = guu.mul_add(wv, -guv * wu) / det;
298        let bend = n.dot(d[2][0] * (a * a) + d[1][1] * (2.0 * a * b) + d[0][2] * (b * b));
299        2.0 * slant.abs() <= SEPARATION * LEAF * self.scale * bend.abs()
300    }
301
302    /// Newton on `S(u, v) = origin + t dir` from the middle of `leaf`,
303    /// recording the hit when it lands in the leaf. Whether it converged,
304    /// in the leaf or out of it: a stall (the ray along the tangent plane
305    /// at a step) or no convergence leaves the leaf unsettled.
306    fn solve(
307        &self,
308        leaf: &Patch,
309        origin: Point3,
310        dir: Vec3,
311        reach: f64,
312        out: &mut RayHits,
313    ) -> bool {
314        let (mut u, mut v) = (
315            f64::midpoint(leaf.u.0, leaf.u.1),
316            f64::midpoint(leaf.v.0, leaf.v.1),
317        );
318        let (du, dv) = (self.surface.domain_u(), self.surface.domain_v());
319        let mut t = dir.dot(self.surface.evaluate(u, v) - origin);
320        for _ in 0..32 {
321            let d = self.surface.derivatives(u, v, 1);
322            let (s, su, sv) = (d[0][0], d[1][0], d[0][1]);
323            let f = s - Vec3::new(origin.x(), origin.y(), origin.z()) - dir * t;
324            let normal = su.cross(sv);
325            let det = -normal.dot(dir);
326            if f.length() <= 1e-12 * self.scale {
327                let slack_u = 1e-9 * (du.1 - du.0);
328                let slack_v = 1e-9 * (dv.1 - dv.0);
329                if u < leaf.u.0 - slack_u
330                    || u > leaf.u.1 + slack_u
331                    || v < leaf.v.0 - slack_v
332                    || v > leaf.v.1 + slack_v
333                    || t < -reach
334                {
335                    return true;
336                }
337                out.grazed |= self.second_crossing_near(u, v, dir);
338                let point = origin + dir * t;
339                if !out.hits.iter().any(|h| (h.point - point).length() <= reach) {
340                    out.hits.push(RayHit { u, v, t, point });
341                }
342                return true;
343            }
344            if det.abs() <= 1e-300 {
345                break;
346            }
347            // Cramer's rule on [su sv -dir] x = -f.
348            let neg_dir = -dir;
349            let rhs = -f;
350            let x_u = rhs.cross(sv).dot(neg_dir) / det;
351            let x_v = su.cross(rhs).dot(neg_dir) / det;
352            let x_t = normal.dot(rhs) / det;
353            u = (u + x_u).clamp(du.0, du.1);
354            v = (v + x_v).clamp(dv.0, dv.1);
355            t += x_t;
356        }
357        false
358    }
359}
360
361#[cfg(test)]
362mod tests {
363    #![allow(clippy::unwrap_used)]
364
365    use super::*;
366
367    /// The lid `z = 100 u (1 - u)^4 v (1 - v)` over the unit square, flat
368    /// along its rim and peaking at 2.048 at `(0.2, 0.5)`.
369    fn lid() -> NurbsSurface {
370        let rows = (0..6)
371            .map(|i| {
372                (0..3)
373                    .map(|j| {
374                        let z = if (i, j) == (1, 1) { 10.0 } else { 0.0 };
375                        Point3::new(f64::from(i) / 5.0, f64::from(j) / 2.0, z)
376                    })
377                    .collect()
378            })
379            .collect();
380        NurbsSurface::new(
381            5,
382            2,
383            [vec![0.0; 6], vec![1.0; 6]].concat(),
384            vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
385            rows,
386            vec![vec![1.0; 3]; 6],
387        )
388        .unwrap()
389    }
390
391    #[test]
392    fn a_ray_up_through_the_lids_peak_meets_it_there() {
393        let lid = RaySurface::new(&lid()).unwrap();
394        let up = Vec3::new(0.0, 0.0, 1.0);
395        let at_peak = lid.hits(Point3::new(0.2, 0.5, -1.0), up, 1e-9);
396        assert!(!at_peak.grazed);
397        assert_eq!(at_peak.hits.len(), 1);
398        assert!((at_peak.hits[0].point.z() - 2.048).abs() < 1e-12);
399        assert!((at_peak.hits[0].t - 3.048).abs() < 1e-12);
400        assert!((at_peak.hits[0].u - 0.2).abs() < 1e-9 && (at_peak.hits[0].v - 0.5).abs() < 1e-9);
401
402        let beside = lid.hits(Point3::new(1.5, 0.5, -1.0), up, 1e-9);
403        assert!(beside.hits.is_empty() && !beside.grazed);
404        let behind = lid.hits(Point3::new(0.2, 0.5, 3.0), up, 1e-9);
405        assert!(behind.hits.is_empty());
406    }
407
408    /// A level ray at `z = 1.5` along `u` through `v = 0.5` crosses the
409    /// lid's bulge twice, where `100 u (1 - u)^4 / 4 = 1.5`.
410    #[test]
411    fn a_level_ray_crosses_the_bulge_twice() {
412        let lid = RaySurface::new(&lid()).unwrap();
413        let across = lid.hits(Point3::new(-1.0, 0.5, 1.5), Vec3::new(1.0, 0.0, 0.0), 1e-9);
414        assert!(!across.grazed);
415        assert_eq!(across.hits.len(), 2, "{:?}", across.hits);
416        for h in &across.hits {
417            let u = h.point.x();
418            assert!((25.0 * u * (1.0 - u).powi(4) - 1.5).abs() < 1e-9, "{h:?}");
419        }
420    }
421
422    #[test]
423    fn a_ray_touching_the_peak_grazes() {
424        let lid = RaySurface::new(&lid()).unwrap();
425        let level = lid.hits(
426            Point3::new(-1.0, 0.5, 2.048),
427            Vec3::new(1.0, 0.0, 0.0),
428            1e-9,
429        );
430        assert!(level.grazed, "{level:?}");
431    }
432
433    /// A unit biquadratic patch with only its middle control point raised,
434    /// to 4000: a level ray at 500 along u through v = 0.5 crosses its
435    /// spike twice, where 4000 u (1 - u) = 500 at v = 0.5, so at
436    /// u = (1 ± 1/√2) / 2.
437    #[test]
438    fn a_level_ray_crosses_a_tall_spike() {
439        let rows = (0..3)
440            .map(|i| {
441                (0..3)
442                    .map(|j| {
443                        let z = if (i, j) == (1, 1) { 4000.0 } else { 0.0 };
444                        Point3::new(f64::from(i) / 2.0, f64::from(j) / 2.0, z)
445                    })
446                    .collect()
447            })
448            .collect();
449        let knots = vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0];
450        let spike =
451            NurbsSurface::new(2, 2, knots.clone(), knots, rows, vec![vec![1.0; 3]; 3]).unwrap();
452        let ray = RaySurface::new(&spike).unwrap();
453        let across = ray.hits(
454            Point3::new(-1.0, 0.5, 500.0),
455            Vec3::new(1.0, 0.0, 0.0),
456            1e-9,
457        );
458        assert!(!across.grazed);
459        let mut xs: Vec<f64> = across.hits.iter().map(|h| h.point.x()).collect();
460        xs.sort_by(f64::total_cmp);
461        let root = std::f64::consts::FRAC_1_SQRT_2;
462        assert_eq!(xs.len(), 2, "{xs:?}");
463        assert!((xs[0] - (1.0 - root) / 2.0).abs() < 1e-9, "{xs:?}");
464        assert!((xs[1] - f64::midpoint(1.0, root)).abs() < 1e-9, "{xs:?}");
465    }
466
467    /// The saddle `(u, v, (u - 1/2)(v - 1/2))` over the unit square, beside
468    /// a span reaching to `x = 2000` that makes the whole saddle one leaf.
469    /// A level ray from its middle at height 0.04 along `(1, 1, 0)` crosses
470    /// it at `(0.7, 0.7)`, though at the leaf's middle the ray runs along
471    /// the tangent plane and a solve from there stalls.
472    #[test]
473    fn a_ray_level_with_a_saddles_middle_crosses_it() {
474        let p = Point3::new;
475        let rows = vec![
476            vec![p(0.0, 0.0, 0.25), p(0.0, 1.0, -0.25)],
477            vec![p(1.0, 0.0, -0.25), p(1.0, 1.0, 0.25)],
478            vec![p(2000.0, 0.0, 0.0), p(2000.0, 1.0, 0.0)],
479        ];
480        let s = NurbsSurface::new(
481            1,
482            1,
483            vec![0.0, 0.0, 1.0, 2.0, 2.0],
484            vec![0.0, 0.0, 1.0, 1.0],
485            rows,
486            vec![vec![1.0; 2]; 3],
487        )
488        .unwrap();
489        let ray = RaySurface::new(&s).unwrap();
490        let dir = Vec3::new(1.0, 1.0, 0.0).normalize().unwrap();
491        let found = ray.hits(p(0.5, 0.5, 0.04), dir, 1e-9);
492        assert_eq!(found.hits.len(), 1, "{:?}", found.grazed);
493        assert!((found.hits[0].point - p(0.7, 0.7, 0.04)).length() < 1e-9);
494    }
495
496    /// A surface whose `u` knots run `[-2, -1, 0, 1, 2, 3]`, unclamped, is
497    /// not taken: its end patches as decomposed are not the surface.
498    #[test]
499    fn an_unclamped_surface_is_not_ready() {
500        let rows = (0..3)
501            .map(|i| {
502                (0..2)
503                    .map(|j| Point3::new(0.5 * f64::from(i), f64::from(j), 0.0))
504                    .collect()
505            })
506            .collect();
507        let s = NurbsSurface::new(
508            2,
509            1,
510            vec![-2.0, -1.0, 0.0, 1.0, 2.0, 3.0],
511            vec![0.0, 0.0, 1.0, 1.0],
512            rows,
513            vec![vec![1.0; 2]; 3],
514        )
515        .unwrap();
516        assert!(RaySurface::new(&s).is_none());
517    }
518
519    #[test]
520    fn a_surface_with_a_negative_weight_is_not_ready() {
521        let mut weights = vec![vec![1.0; 3]; 6];
522        weights[2][1] = -1.0;
523        let s = lid();
524        let s = NurbsSurface::new(
525            5,
526            2,
527            s.knots_u().to_vec(),
528            s.knots_v().to_vec(),
529            s.control_points().to_vec(),
530            weights,
531        )
532        .unwrap();
533        assert!(RaySurface::new(&s).is_none());
534    }
535}