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        collect_occluder_points(std::slice::from_ref(self))
116    }
117}
118
119/// Implements the constructor and zero-allocation tracing entry points for
120/// [`RayTraceScene`].
121impl RayTraceScene {
122    /// Creates a new scene taking ownership of `occluders` and precomputing
123    /// the `(center, radius)` shadow bounding spheres used by
124    /// [`soft_shadow_factor`].
125    ///
126    /// # Arguments
127    ///
128    /// - `Vec<Occluder>` - All occluding surfaces in the scene.
129    ///
130    /// # Returns
131    ///
132    /// - `RayTraceScene` - The new scene with precomputed shadow data.
133    pub fn new(occluders: Vec<Occluder>) -> RayTraceScene {
134        let shadow_points: Vec<(Vector3D, f64)> = collect_occluder_points(&occluders);
135        RayTraceScene {
136            occluders,
137            shadow_points,
138        }
139    }
140
141    /// Iteratively traces a ray through the scene and returns the final
142    /// shaded color, using the [`RAYTRACE_DEFAULT_MAX_BOUNCES`] constant as
143    /// the bounce limit.
144    ///
145    /// Performs no heap allocation per ray or per bounce: the shadow
146    /// bounding spheres precomputed at construction are reused, and no
147    /// [`Material`] is cloned. Use [`RayTraceScene::trace_with_bounces`] to
148    /// override the bounce limit.
149    ///
150    /// # Arguments
151    ///
152    /// - `Ray` - The ray to trace.
153    /// - `&LightingUniforms` - Lighting parameters used during shading.
154    ///
155    /// # Returns
156    ///
157    /// - `Vector3D` - The final traced color.
158    pub fn trace(&self, ray: Ray, lights: &LightingUniforms) -> Vector3D {
159        self.trace_with_bounces(ray, lights, RAYTRACE_DEFAULT_MAX_BOUNCES)
160    }
161
162    /// Iteratively traces a ray through the scene with an explicit bounce
163    /// limit and returns the final shaded color.
164    ///
165    /// On a miss the ambient color scaled by the accumulated specular
166    /// throughput is added. On a hit the surface material is evaluated with
167    /// [`LightingUniforms::shade`] and, when the hit material has a
168    /// non-zero specular component, the trace continues with a reflected
169    /// ray up to `max_bounces` times (incrementing the ray's `depth` field
170    /// per bounce).
171    ///
172    /// # Arguments
173    ///
174    /// - `Ray` - The ray to trace.
175    /// - `&LightingUniforms` - Lighting parameters used during shading.
176    /// - `u32` - The maximum number of bounces allowed for this ray.
177    ///
178    /// # Returns
179    ///
180    /// - `Vector3D` - The final traced color.
181    pub fn trace_with_bounces(
182        &self,
183        ray: Ray,
184        lights: &LightingUniforms,
185        max_bounces: u32,
186    ) -> Vector3D {
187        trace_bounces(
188            ray,
189            self.get_occluders(),
190            &self.shadow_points,
191            lights,
192            max_bounces,
193        )
194    }
195
196    /// Finds the closest intersection between a ray and the scene
197    /// occluders.
198    ///
199    /// The winning occluder's [`Material`] is cloned exactly once, when the
200    /// returned [`Hit`] is constructed; losing candidates are never cloned.
201    ///
202    /// # Arguments
203    ///
204    /// - `&Ray` - The ray to test.
205    ///
206    /// # Returns
207    ///
208    /// - `Option<Hit>` - The closest hit, or `None` if the ray misses.
209    pub fn closest_hit(&self, ray: &Ray) -> Option<Hit> {
210        let occluders: &[Occluder] = self.get_occluders();
211        closest_hit_indexed(ray, occluders).map(
212            |(index, t, position, normal): (usize, f64, Vector3D, Vector3D)| Hit {
213                t,
214                position,
215                normal,
216                material: occluders[index].get_material().clone(),
217            },
218        )
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225
226    /// A ray that escapes an empty scene returns the ambient color.
227    #[test]
228    fn trace_miss_returns_ambient() {
229        let eye: Vector3D = Vector3D::new(0.0, 0.0, 0.0);
230        let mut lights: LightingUniforms = LightingUniforms::with_eye(eye);
231        lights.set_ambient(Vector3D::new(0.2, 0.4, 0.6));
232        let ray: Ray = Ray::new(Vector3D::new(0.0, 0.0, 0.0), Vector3D::new(1.0, 0.0, 0.0));
233        let occluders: Vec<Occluder> = Vec::new();
234        let scene: RayTraceScene = RayTraceScene::new(occluders);
235        let color: Vector3D = scene.trace(ray, &lights);
236        assert!(
237            (color.get_x() - 0.2).abs() < EPSILON,
238            "expected ambient red 0.2, got {}",
239            color.get_x(),
240        );
241        assert!(
242            (color.get_y() - 0.4).abs() < EPSILON,
243            "expected ambient green 0.4, got {}",
244            color.get_y(),
245        );
246        assert!(
247            (color.get_z() - 0.6).abs() < EPSILON,
248            "expected ambient blue 0.6, got {}",
249            color.get_z(),
250        );
251    }
252
253    /// A ray that hits an emissive sphere returns the sphere's emissive
254    /// color (no shadow attenuation because the surface IS the light).
255    #[test]
256    fn trace_emissive_sphere() {
257        let eye: Vector3D = Vector3D::new(0.0, 0.0, 5.0);
258        let mut lights: LightingUniforms = LightingUniforms::with_eye(eye);
259        lights.set_ambient(Vector3D::zero());
260        let sphere_material: Material = Material::emissive(Vector3D::new(1.0, 0.0, 0.0));
261        let sphere: Occluder = Occluder::sphere(Vector3D::zero(), 1.0, sphere_material);
262        let occluders: Vec<Occluder> = vec![sphere];
263        let scene: RayTraceScene = RayTraceScene::new(occluders);
264        let ray: Ray = Ray::new(Vector3D::new(0.0, 0.0, 5.0), Vector3D::new(0.0, 0.0, -1.0));
265        let color: Vector3D = scene.trace(ray, &lights);
266        assert!(
267            (color.get_x() - 1.0).abs() < EPSILON,
268            "expected emissive red 1.0, got {}",
269            color.get_x(),
270        );
271        assert!(
272            color.get_y().abs() < EPSILON,
273            "expected emissive green 0.0, got {}",
274            color.get_y(),
275        );
276        assert!(
277            color.get_z().abs() < EPSILON,
278            "expected emissive blue 0.0, got {}",
279            color.get_z(),
280        );
281    }
282
283    /// A ray that hits a mirror sphere (Phong specular = 1.0) reflects
284    /// once and lands on an emissive sphere, returning a mixed color.
285    #[test]
286    fn trace_reflection_single_bounce() {
287        let eye: Vector3D = Vector3D::new(0.0, 0.0, 10.0);
288        let mut lights: LightingUniforms = LightingUniforms::with_eye(eye);
289        lights.set_ambient(Vector3D::zero());
290        let mirror_material: Material = Material::phong(Vector3D::zero(), 1.0, 32.0);
291        let mirror: Occluder = Occluder::sphere(Vector3D::zero(), 1.0, mirror_material);
292        // Emissive sphere along +z past the mirror. Ray bounces straight
293        // back along +z after hitting the dead-center +z hemisphere, so
294        // place the emissive on that line.
295        let emissive_material: Material = Material::emissive(Vector3D::new(0.0, 1.0, 0.0));
296        let emissive: Occluder =
297            Occluder::sphere(Vector3D::new(0.0, 0.0, 15.0), 1.0, emissive_material);
298        let occluders: Vec<Occluder> = vec![mirror, emissive];
299        let scene: RayTraceScene = RayTraceScene::new(occluders);
300        let ray: Ray = Ray::new(Vector3D::new(0.0, 0.0, 10.0), Vector3D::new(0.0, 0.0, -1.0));
301        let color: Vector3D = scene.trace(ray, &lights);
302        assert!(
303            color.get_y() > 0.0,
304            "expected bounce to bring back some green, got {}",
305            color.get_y(),
306        );
307        assert!(
308            color.get_x().abs() < EPSILON,
309            "expected red ~0 (no red light), got {}",
310            color.get_x(),
311        );
312        assert!(
313            color.get_z().abs() < EPSILON,
314            "expected blue ~0 (no blue light), got {}",
315            color.get_z(),
316        );
317    }
318
319    /// Builds the scene mirrored from the /raytrace example: a ground
320    /// AABB, a mirror sphere, and an emissive sphere, lit by one
321    /// directional sun with a fixed yaw.
322    ///
323    /// # Returns
324    ///
325    /// - `(Vec<Occluder>, LightingUniforms)` - The scene occluders and the
326    ///   lighting uniforms.
327    fn demo_scene() -> (Vec<Occluder>, LightingUniforms) {
328        let ground: Occluder = Occluder::aabb(
329            Vector3D::new(-5.0, -0.6, -5.0),
330            Vector3D::new(5.0, -0.5, 5.0),
331            Material::phong(Vector3D::new(0.30, 0.32, 0.36), 0.30, 24.0),
332        );
333        let mirror: Occluder = Occluder::sphere(
334            Vector3D::new(0.0, 0.4, 0.0),
335            0.9,
336            Material::phong(Vector3D::new(0.05, 0.05, 0.06), 1.0, 64.0),
337        );
338        let emissive: Occluder = Occluder::sphere(
339            Vector3D::new(1.6, 0.6, -1.4),
340            0.45,
341            Material::emissive(Vector3D::new(1.0, 0.45, 0.10)),
342        );
343        let occluders: Vec<Occluder> = vec![ground, mirror, emissive];
344        let eye: Vector3D = Vector3D::new(0.0, 0.8, 3.5);
345        let yaw: f64 = 0.7;
346        let light_dir: Vector3D = Vector3D::new(-yaw.cos(), -0.5, -yaw.sin()).normalized();
347        let sun: Light = Light::new_directional(light_dir, Vector3D::new(1.0, 0.95, 0.85));
348        let mut lights: LightingUniforms = LightingUniforms::with_eye(eye);
349        lights.set_ambient(Vector3D::new(0.10, 0.10, 0.14));
350        lights.add_light(sun);
351        (occluders, lights)
352    }
353
354    /// `RayTraceScene::trace` is exactly [`RayTraceScene::trace_with_bounces`]
355    /// evaluated at the [`RAYTRACE_DEFAULT_MAX_BOUNCES`] limit.
356    #[test]
357    fn trace_matches_trace_with_bounces_at_default_limit() {
358        let (occluders, lights): (Vec<Occluder>, LightingUniforms) = demo_scene();
359        let scene: RayTraceScene = RayTraceScene::new(occluders);
360        let ray: Ray = Ray::new(
361            Vector3D::new(0.0, 0.8, 3.5),
362            Vector3D::new(0.0, -0.4, -3.5).normalized(),
363        );
364        let default_color: Vector3D = scene.trace(ray.clone(), &lights);
365        let explicit_color: Vector3D =
366            scene.trace_with_bounces(ray, &lights, RAYTRACE_DEFAULT_MAX_BOUNCES);
367        assert_eq!(
368            default_color, explicit_color,
369            "trace must equal trace_with_bounces at the default bounce limit",
370        );
371    }
372
373    /// `RayTraceScene::closest_hit` matches the analytic intersection
374    /// distance for a dead-center ray and returns `None` on a miss.
375    #[test]
376    fn closest_hit_returns_analytic_t() {
377        let (occluders, _lights): (Vec<Occluder>, LightingUniforms) = demo_scene();
378        let scene: RayTraceScene = RayTraceScene::new(occluders);
379        let dead_center: Ray =
380            Ray::new(Vector3D::new(0.0, 0.4, 5.0), Vector3D::new(0.0, 0.0, -1.0));
381        let expected_t: f64 = 5.0 - 0.9;
382        let hit: Option<Hit> = scene.closest_hit(&dead_center);
383        assert!(hit.is_some(), "expected dead-center ray to hit the mirror");
384        assert!(
385            (hit.expect("checked above").get_t() - expected_t).abs() < 1e-9,
386            "expected analytic t {expected_t}",
387        );
388        let away: Ray = Ray::new(Vector3D::new(0.0, 0.4, 5.0), Vector3D::new(0.0, 0.0, 1.0));
389        assert!(
390            scene.closest_hit(&away).is_none(),
391            "expected ray pointing away from the scene to miss",
392        );
393    }
394}