Skip to main content

euv_engine/raytracing/
impl.rs

1use super::*;
2
3/// Implements factory constructors and accessors for [`Ray`] and
4/// [`Occluder`].
5impl Ray {
6    /// Creates a new ray starting at `origin` pointing in `direction`.
7    ///
8    /// `t_min` and `t_max` default to [`RAYTRACE_DEFAULT_T_MIN`] and
9    /// [`RAYTRACE_DEFAULT_T_MAX`]. `depth` defaults to 0.
10    ///
11    /// # Arguments
12    ///
13    /// - `Vector3D` - The ray origin.
14    /// - `Vector3D` - The unit direction.
15    ///
16    /// # Returns
17    ///
18    /// - `Ray` - The new ray.
19    pub fn new(origin: Vector3D, direction: Vector3D) -> Ray {
20        Ray {
21            origin,
22            direction,
23            t_min: RAYTRACE_DEFAULT_T_MIN,
24            t_max: RAYTRACE_DEFAULT_T_MAX,
25            depth: 0,
26        }
27    }
28
29    /// Computes the world-space point at distance `t` along this ray.
30    ///
31    /// # Arguments
32    ///
33    /// - `f64` - The ray parameter.
34    ///
35    /// # Returns
36    ///
37    /// - `Vector3D` - `origin + direction * t`.
38    pub fn at(&self, t: f64) -> Vector3D {
39        self.get_origin() + self.get_direction().scaled(t)
40    }
41
42    /// Returns a clone of this ray with `depth` replaced by `depth`.
43    ///
44    /// # Arguments
45    ///
46    /// - `u32` - The new recursion depth.
47    ///
48    /// # Returns
49    ///
50    /// - `Ray` - The cloned ray with updated depth.
51    pub fn with_depth(&self, depth: u32) -> Ray {
52        Ray {
53            origin: self.get_origin(),
54            direction: self.get_direction(),
55            t_min: self.get_t_min(),
56            t_max: self.get_t_max(),
57            depth,
58        }
59    }
60}
61
62/// Implements factory constructors for [`Occluder`].
63impl Occluder {
64    /// Creates a spherical occluder centered at `center` with `radius`.
65    ///
66    /// # Arguments
67    ///
68    /// - `Vector3D` - The sphere center.
69    /// - `f64` - The sphere radius.
70    /// - `Material` - The surface material.
71    ///
72    /// # Returns
73    ///
74    /// - `Occluder` - The new sphere occluder.
75    pub fn sphere(center: Vector3D, radius: f64, material: Material) -> Occluder {
76        Occluder {
77            kind: OccluderKind::Sphere,
78            center,
79            extent: Vector3D::new(radius, radius, radius),
80            material,
81        }
82    }
83
84    /// Creates an axis-aligned bounding-box occluder from `min` to `max`.
85    ///
86    /// # Arguments
87    ///
88    /// - `Vector3D` - The AABB minimum corner.
89    /// - `Vector3D` - The AABB maximum corner.
90    /// - `Material` - The surface material.
91    ///
92    /// # Returns
93    ///
94    /// - `Occluder` - The new AABB occluder.
95    pub fn aabb(min: Vector3D, max: Vector3D, material: Material) -> Occluder {
96        Occluder {
97            kind: OccluderKind::Aabb,
98            center: min,
99            extent: max,
100            material,
101        }
102    }
103
104    /// Returns a list of `(center, radius)` sphere tuples approximating
105    /// this occluder, suitable for [`soft_shadow_factor`].
106    ///
107    /// For sphere occluders this returns `(center, radius)`. For AABB
108    /// occluders the bounding sphere is computed conservatively from the
109    /// AABB extents.
110    ///
111    /// # Returns
112    ///
113    /// - `Vec<(Vector3D, f64)>` - One bounding sphere per occluder.
114    pub fn occluder_points(&self) -> Vec<(Vector3D, f64)> {
115        let (mn, mx): (Vector3D, Vector3D) = occluder_aabb_extents(self);
116        let cx: f64 = (mn.get_x() + mx.get_x()) * 0.5;
117        let cy: f64 = (mn.get_y() + mx.get_y()) * 0.5;
118        let cz: f64 = (mn.get_z() + mx.get_z()) * 0.5;
119        let ex: f64 = (mx.get_x() - mn.get_x()) * 0.5;
120        let ey: f64 = (mx.get_y() - mn.get_y()) * 0.5;
121        let ez: f64 = (mx.get_z() - mn.get_z()) * 0.5;
122        let r: f64 = (ex * ex + ey * ey + ez * ez).sqrt();
123        vec![(Vector3D::new(cx, cy, cz), r)]
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    /// A ray that escapes the scene (no occluders) returns the ambient color.
132    #[test]
133    fn trace_miss_returns_ambient() {
134        let eye: Vector3D = Vector3D::new(0.0, 0.0, 0.0);
135        let mut lights: LightingUniforms = LightingUniforms::with_eye(eye);
136        lights.set_ambient(Vector3D::new(0.2, 0.4, 0.6));
137        let ray: Ray = Ray::new(Vector3D::new(0.0, 0.0, 0.0), Vector3D::new(1.0, 0.0, 0.0));
138        let occluders: [Occluder; 0] = [];
139        let color: Vector3D = trace(ray, &occluders, &lights, RAYTRACE_DEFAULT_MAX_BOUNCES);
140        assert!(
141            (color.get_x() - 0.2).abs() < EPSILON,
142            "expected ambient red 0.2, got {}",
143            color.get_x(),
144        );
145        assert!(
146            (color.get_y() - 0.4).abs() < EPSILON,
147            "expected ambient green 0.4, got {}",
148            color.get_y(),
149        );
150        assert!(
151            (color.get_z() - 0.6).abs() < EPSILON,
152            "expected ambient blue 0.6, got {}",
153            color.get_z(),
154        );
155    }
156
157    /// A ray that hits an emissive sphere returns the sphere's emissive
158    /// color (no shadow attenuation because the surface IS the light).
159    #[test]
160    fn trace_emissive_sphere() {
161        let eye: Vector3D = Vector3D::new(0.0, 0.0, 5.0);
162        let mut lights: LightingUniforms = LightingUniforms::with_eye(eye);
163        lights.set_ambient(Vector3D::zero());
164        let sphere_material: Material = Material::emissive(Vector3D::new(1.0, 0.0, 0.0));
165        let sphere: Occluder = Occluder::sphere(Vector3D::zero(), 1.0, sphere_material);
166        let ray: Ray = Ray::new(Vector3D::new(0.0, 0.0, 5.0), Vector3D::new(0.0, 0.0, -1.0));
167        let occluders: [Occluder; 1] = [sphere];
168        let color: Vector3D = trace(ray, &occluders, &lights, RAYTRACE_DEFAULT_MAX_BOUNCES);
169        assert!(
170            (color.get_x() - 1.0).abs() < EPSILON,
171            "expected emissive red 1.0, got {}",
172            color.get_x(),
173        );
174        assert!(
175            color.get_y().abs() < EPSILON,
176            "expected emissive green 0.0, got {}",
177            color.get_y(),
178        );
179        assert!(
180            color.get_z().abs() < EPSILON,
181            "expected emissive blue 0.0, got {}",
182            color.get_z(),
183        );
184    }
185
186    /// A ray that hits a mirror sphere (Phong specular = 1.0) reflects
187    /// once and lands on an emissive sphere, returning a mixed color.
188    #[test]
189    fn trace_reflection_single_bounce() {
190        let eye: Vector3D = Vector3D::new(0.0, 0.0, 10.0);
191        let mut lights: LightingUniforms = LightingUniforms::with_eye(eye);
192        lights.set_ambient(Vector3D::zero());
193        let mirror_material: Material = Material::phong(Vector3D::zero(), 1.0, 32.0);
194        let mirror: Occluder = Occluder::sphere(Vector3D::zero(), 1.0, mirror_material);
195        // Emissive sphere along +z past the mirror. Ray bounces straight
196        // back along +z after hitting the dead-center +z hemisphere, so
197        // place the emissive on that line.
198        let emissive_material: Material = Material::emissive(Vector3D::new(0.0, 1.0, 0.0));
199        let emissive: Occluder =
200            Occluder::sphere(Vector3D::new(0.0, 0.0, 15.0), 1.0, emissive_material);
201        let ray: Ray = Ray::new(Vector3D::new(0.0, 0.0, 10.0), Vector3D::new(0.0, 0.0, -1.0));
202        let occluders: [Occluder; 2] = [mirror, emissive];
203        let color: Vector3D = trace(ray, &occluders, &lights, RAYTRACE_DEFAULT_MAX_BOUNCES);
204        assert!(
205            color.get_y() > 0.0,
206            "expected bounce to bring back some green, got {}",
207            color.get_y(),
208        );
209        assert!(
210            color.get_x().abs() < EPSILON,
211            "expected red ~0 (no red light), got {}",
212            color.get_x(),
213        );
214        assert!(
215            color.get_z().abs() < EPSILON,
216            "expected blue ~0 (no blue light), got {}",
217            color.get_z(),
218        );
219    }
220}