Skip to main content

rayx/
lib.rs

1//! RayX: a small, fast ray intersection library.
2//!
3//! `rayx` is a small Rust ray intersection library using **Baldwin-Weber's Fast Ray-Triangle Intersections by Coordinate Transformation** ([JCGT 2016](https://jcgt.org/published/0005/03/03/)).
4//!
5//! The precompute costs a little memory per triangle but reduces per-ray arithmetic.
6//!
7//! ## Performance
8//!
9//! Baldwin-Weber consistently outperforms Moller-Trumbore in intersection time by 10-45%,
10//! depending on the scene, especially at f32 precision. The crossover point for the initialization overhead
11//! is at about 5 rays per triangle. Learn more in the [README.md](https://github.com/p-sira/rayx?tab=readme-ov-file#performance).
12//!
13//! ## Usage
14//!
15//! ```
16//! use rayx::{Ray, Triangle};
17//!
18//! let tri = Triangle::<f64>::new([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]).unwrap();
19//!
20//! let ray = Ray::new([0.25, 0.25, 1.0], [0.0, 0.0, -1.0]);
21//! let hit = tri.intersect(ray, 0.0, 10.0).unwrap();
22//! # assert!((hit.t - 1.0).abs() < 1e-12);
23//! # assert!((hit.u - 0.25).abs() < 1e-12);
24//! # assert!((hit.v - 0.25).abs() < 1e-12);
25//! ```
26
27#![cfg_attr(not(feature = "std"), no_std)]
28#![cfg_attr(docsrs, feature(doc_cfg))]
29
30use core::fmt;
31
32mod base;
33pub mod intersect;
34
35pub use base::{Hit, Ray};
36
37use nalgebra::{RealField, Vector3};
38use num_traits::Float;
39
40/// Triangle with precomputed transform coefficients for fast intersection tests.
41#[derive(Clone, Copy, Debug, PartialEq, Eq)]
42pub struct Triangle<T: Float + RealField = f32> {
43    m: nalgebra::Matrix3<T>,
44    v: nalgebra::Vector3<T>,
45}
46
47impl<T: Float + RealField> Triangle<T> {
48    /// Build a triangle and store the precomputed transform.
49    ///
50    /// Note: The vertices are not stored.
51    #[inline]
52    pub fn new(
53        v1: impl Into<Vector3<T>>,
54        v2: impl Into<Vector3<T>>,
55        v3: impl Into<Vector3<T>>,
56    ) -> Result<Self, TriangleError> {
57        let m = intersect::compute_baldwin_weber_transform(v1.into(), v2.into(), v3.into())?;
58        Ok(Self {
59            m: m.fixed_view::<3, 3>(0, 0).into(),
60            v: m.column(3).into(),
61        })
62    }
63
64    /// Intersect ray with this triangle using the precomputed transform.
65    ///
66    /// Returns `Some(Hit)` if the ray intersects within `[t_min, t_max]`.
67    #[inline]
68    pub fn intersect(&self, ray: Ray<T>, t_min: T, t_max: T) -> Option<Hit<T>> {
69        intersect::intersect_baldwin_weber(&self.m, &self.v, ray, t_min, t_max)
70    }
71
72    /// Reconstruct the original triangle vertices from the precomputed transform.
73    #[inline]
74    pub fn reconstruct_vertices(&self) -> [Vector3<T>; 3] {
75        let inv_m = self
76            .m
77            .try_inverse()
78            .expect("Cannot invert the triangle matrix");
79
80        let v1 = inv_m * (-self.v);
81        let v2 = v1 + inv_m.column(0);
82        let v3 = v1 + inv_m.column(1);
83
84        [v1, v2, v3]
85    }
86
87    #[inline]
88    pub fn m(&self) -> nalgebra::Matrix3<T> {
89        self.m
90    }
91
92    #[inline]
93    pub fn v(&self) -> nalgebra::Vector3<T> {
94        self.v
95    }
96}
97
98#[derive(Clone, Copy, Debug, PartialEq)]
99pub enum TriangleError {
100    Degenerate,
101}
102
103impl fmt::Display for TriangleError {
104    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105        match self {
106            TriangleError::Degenerate => write!(f, "Triangle is degenerate (zero surface area)"),
107        }
108    }
109}
110
111#[cfg(feature = "std")]
112impl std::error::Error for TriangleError {}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    fn approx<T: Float>(a: T, b: T, tol: T) -> bool {
119        (a - b).abs() <= tol
120    }
121
122    #[test]
123    fn degenerate_triangle_rejected() {
124        let zeros = Vector3::new(0.0, 0.0, 0.0);
125        assert_eq!(
126            Triangle::new(zeros, zeros, zeros),
127            Err(TriangleError::Degenerate)
128        );
129    }
130
131    #[test]
132    fn basic_hit_matches_expected_barycentrics() {
133        // Canonical triangle in z=0 plane: (0,0,0), (1,0,0), (0,1,0)
134        let tri = Triangle::new([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]).unwrap();
135
136        let ray = Ray::new([0.25, 0.25, 1.0], [0.0, 0.0, -1.0]);
137        let hit = tri.intersect(ray, 0.0, 10.0).unwrap();
138        assert!(approx(hit.t, 1.0, 1e-12));
139        assert!(approx(hit.u, 0.25, 1e-12));
140        assert!(approx(hit.v, 0.25, 1e-12));
141        assert!(approx(hit.w(), 0.5, 1e-12));
142    }
143
144    #[test]
145    fn miss_outside_triangle() {
146        let tri = Triangle::new(
147            Vector3::new(0.0, 0.0, 0.0),
148            Vector3::new(1.0, 0.0, 0.0),
149            Vector3::new(0.0, 1.0, 0.0),
150        )
151        .unwrap();
152
153        let ray = Ray::new(Vector3::new(0.8, 0.8, 1.0), Vector3::new(0.0, 0.0, -1.0));
154        assert_eq!(tri.intersect(ray, 0.0, 10.0), None);
155    }
156
157    #[test]
158    fn agrees_with_moller_trumbore() {
159        let v0 = Vector3::new(-1.0, 0.0, 0.5);
160        let v1 = Vector3::new(2.0, 0.3, -0.2);
161        let v2 = Vector3::new(0.2, 1.7, 0.1);
162        let tri = Triangle::new(v0, v1, v2).unwrap();
163
164        let rays = [
165            Ray::new(Vector3::new(0.1, 0.2, 2.0), Vector3::new(0.0, 0.0, -1.0)),
166            Ray::new(
167                Vector3::new(10.0, 10.0, 10.0),
168                Vector3::new(-1.0, -1.0, -1.0),
169            ),
170            Ray::new(Vector3::new(0.0, -2.0, 0.0), Vector3::new(0.2, 1.0, 0.1)),
171            Ray::new(Vector3::new(0.3, 0.4, 0.5), Vector3::new(1.0, 0.1, -0.2)),
172        ];
173
174        for ray in rays {
175            let a = tri.intersect(ray, 0.0, 1e9);
176            let b = intersect::intersect_moller_trumbore(v0, v1, v2, ray, 0.0, 1e9);
177            match (a, b) {
178                (None, None) => {}
179                (Some(ha), Some(hb)) => {
180                    assert!(approx(ha.t, hb.t, 1e-12), "t mismatch: {ha:?} vs {hb:?}");
181                    assert!(approx(ha.u, hb.u, 1e-12), "b1 mismatch: {ha:?} vs {hb:?}");
182                    assert!(approx(ha.v, hb.v, 1e-12), "b2 mismatch: {ha:?} vs {hb:?}");
183                }
184                _ => panic!("disagreement: precomputed={a:?}, mt={b:?}"),
185            }
186        }
187    }
188
189    #[test]
190    fn reconstruct_vertices_matches_input() {
191        let v0 = Vector3::new(-1.2, 0.5, 3.1);
192        let v1 = Vector3::new(2.4, -1.1, 0.8);
193        let v2 = Vector3::new(0.3, 4.2, -1.5);
194
195        let tri = Triangle::new(v0, v1, v2).unwrap();
196        let reconstructed = tri.reconstruct_vertices();
197
198        assert!(approx(reconstructed[0].x, v0.x, 1e-12));
199        assert!(approx(reconstructed[0].y, v0.y, 1e-12));
200        assert!(approx(reconstructed[0].z, v0.z, 1e-12));
201
202        assert!(approx(reconstructed[1].x, v1.x, 1e-12));
203        assert!(approx(reconstructed[1].y, v1.y, 1e-12));
204        assert!(approx(reconstructed[1].z, v1.z, 1e-12));
205
206        assert!(approx(reconstructed[2].x, v2.x, 1e-12));
207        assert!(approx(reconstructed[2].y, v2.y, 1e-12));
208        assert!(approx(reconstructed[2].z, v2.z, 1e-12));
209    }
210}