Skip to main content

euv_engine/raytracing/scene/
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    /// Returns a clone of this ray with its parameter range replaced.
62    ///
63    /// The new bounds are taken as given, so an inverted or non-finite
64    /// range is representable; both query entry points reject such a ray
65    /// as untraceable rather than letting `NaN` comparisons decide the
66    /// outcome.
67    ///
68    /// # Arguments
69    ///
70    /// - `f64` - The minimum accepted hit parameter.
71    /// - `f64` - The maximum accepted hit parameter.
72    ///
73    /// # Returns
74    ///
75    /// - `Ray` - The cloned ray with the new range.
76    pub fn with_range(&self, t_min: f64, t_max: f64) -> Ray {
77        Ray {
78            origin: self.get_origin(),
79            direction: self.get_direction(),
80            t_min,
81            t_max,
82            depth: self.get_depth(),
83        }
84    }
85
86    /// Intersects this ray with a triangle using the Moller-Trumbore
87    /// algorithm, keeping only hits inside this ray's `t_min`..=`t_max`
88    /// range.
89    ///
90    /// The test is two-sided: a hit from either face counts, and the
91    /// returned normal always faces against the ray direction. This is the
92    /// range-clamped counterpart of the free
93    /// [`intersect_triangle`](crate::intersect_triangle), which reports the
94    /// raw distance however far away the triangle is.
95    ///
96    /// # Arguments
97    ///
98    /// - `[Vector3D; 3]` - The three triangle vertices in winding order.
99    ///
100    /// # Returns
101    ///
102    /// - `Option<(f64, Vector3D)>` - The hit distance along the ray and the
103    ///   unit normal oriented against the ray direction, or `None` on miss.
104    pub fn intersect_triangle(&self, vertices: [Vector3D; 3]) -> Option<(f64, Vector3D)> {
105        let (t, normal): (f64, Vector3D) = intersect_triangle(self, vertices)?;
106        if t < self.get_t_min() || t > self.get_t_max() {
107            return None;
108        }
109        Some((t, normal))
110    }
111}
112
113/// Implements factory constructors for [`Occluder`].
114impl Occluder {
115    /// Creates a spherical occluder centered at `center` with `radius`.
116    ///
117    /// # Arguments
118    ///
119    /// - `Vector3D` - The sphere center.
120    /// - `f64` - The sphere radius.
121    /// - `Material` - The surface material.
122    ///
123    /// # Returns
124    ///
125    /// - `Occluder` - The new sphere occluder.
126    pub fn sphere(center: Vector3D, radius: f64, material: Material) -> Occluder {
127        Occluder {
128            kind: OccluderKind::Sphere,
129            center,
130            extent: Vector3D::new(radius, radius, radius),
131            vertices: [center; 3],
132            material,
133        }
134    }
135
136    /// Creates an axis-aligned bounding-box occluder from `min` to `max`.
137    ///
138    /// # Arguments
139    ///
140    /// - `Vector3D` - The AABB minimum corner.
141    /// - `Vector3D` - The AABB maximum corner.
142    /// - `Material` - The surface material.
143    ///
144    /// # Returns
145    ///
146    /// - `Occluder` - The new AABB occluder.
147    pub fn aabb(min: Vector3D, max: Vector3D, material: Material) -> Occluder {
148        Occluder {
149            kind: OccluderKind::Aabb,
150            center: min,
151            extent: max,
152            vertices: [min; 3],
153            material,
154        }
155    }
156
157    /// Creates a triangular occluder from its three vertices.
158    ///
159    /// The vertex order fixes the geometric winding; because the
160    /// intersection test is two-sided the winding does not affect which
161    /// faces produce hits, only the raw cross-product normal before it is
162    /// oriented against the incoming ray.
163    ///
164    /// # Arguments
165    ///
166    /// - `Vector3D` - The first triangle vertex.
167    /// - `Vector3D` - The second triangle vertex.
168    /// - `Vector3D` - The third triangle vertex.
169    /// - `Material` - The surface material.
170    ///
171    /// # Returns
172    ///
173    /// - `Occluder` - The new triangle occluder.
174    pub fn triangle(v0: Vector3D, v1: Vector3D, v2: Vector3D, material: Material) -> Occluder {
175        Occluder {
176            kind: OccluderKind::Triangle,
177            center: Vector3D::zero(),
178            extent: Vector3D::zero(),
179            vertices: [v0, v1, v2],
180            material,
181        }
182    }
183
184    /// Returns a list of `(center, radius)` sphere tuples approximating
185    /// this occluder, suitable for [`soft_shadow_factor`].
186    ///
187    /// For sphere occluders this returns `(center, radius)`. For AABB
188    /// occluders the bounding sphere is computed conservatively from the
189    /// AABB extents.
190    ///
191    /// # Returns
192    ///
193    /// - `Vec<(Vector3D, f64)>` - One bounding sphere per occluder.
194    pub fn occluder_points(&self) -> Vec<(Vector3D, f64)> {
195        collect_occluder_points(from_ref(self))
196    }
197}
198
199/// Implements the constructor and zero-allocation tracing entry points for
200/// [`RayTraceScene`].
201impl RayTraceScene {
202    /// Creates a new scene taking ownership of `occluders` and precomputing
203    /// both the bounding volume hierarchy and the `(center, radius)` shadow
204    /// bounding spheres used by [`soft_shadow_factor`].
205    ///
206    /// The hierarchy is built once here; every later query reuses it, so
207    /// the cost of the BVH never appears in a per-ray measurement. The
208    /// public surface is unchanged from the pre-BVH constructor, which
209    /// takes the occluder list by value.
210    ///
211    /// # Arguments
212    ///
213    /// - `Vec<Occluder>` - All occluding surfaces in the scene.
214    ///
215    /// # Returns
216    ///
217    /// - `RayTraceScene` - The new scene with precomputed shadow data.
218    pub fn new(occluders: Vec<Occluder>) -> RayTraceScene {
219        let shadow_points: Vec<(Vector3D, f64)> = collect_occluder_points(&occluders);
220        let (nodes, indices): (Vec<RayTraceNode>, Vec<usize>) = build_bvh(&occluders);
221        RayTraceScene {
222            occluders,
223            nodes,
224            indices,
225            shadow_points,
226        }
227    }
228
229    /// Iteratively traces a ray through the scene and returns the final
230    /// shaded color, using the [`RAYTRACE_DEFAULT_MAX_BOUNCES`] constant as
231    /// the bounce limit.
232    ///
233    /// Performs no heap allocation per ray or per bounce: the shadow
234    /// bounding spheres precomputed at construction are reused, and no
235    /// [`Material`] is cloned. Use [`RayTraceScene::trace_with_bounces`] to
236    /// override the bounce limit.
237    ///
238    /// # Arguments
239    ///
240    /// - `Ray` - The ray to trace.
241    /// - `&LightingUniforms` - Lighting parameters used during shading.
242    ///
243    /// # Returns
244    ///
245    /// - `Vector3D` - The final traced color.
246    pub fn trace(&self, ray: Ray, lights: &LightingUniforms) -> Vector3D {
247        self.trace_with_bounces(ray, lights, RAYTRACE_DEFAULT_MAX_BOUNCES)
248    }
249
250    /// Iteratively traces a ray through the scene with an explicit bounce
251    /// limit and returns the final shaded color.
252    ///
253    /// On a miss the ambient color scaled by the accumulated specular
254    /// throughput is added. On a hit the surface material is evaluated with
255    /// [`LightingUniforms::shade`] and, when the hit material has a
256    /// non-zero specular component, the trace continues with a reflected
257    /// ray up to `max_bounces` times (incrementing the ray's `depth` field
258    /// per bounce).
259    ///
260    /// # Arguments
261    ///
262    /// - `Ray` - The ray to trace.
263    /// - `&LightingUniforms` - Lighting parameters used during shading.
264    /// - `u32` - The maximum number of bounces allowed for this ray.
265    ///
266    /// # Returns
267    ///
268    /// - `Vector3D` - The final traced color.
269    pub fn trace_with_bounces(
270        &self,
271        ray: Ray,
272        lights: &LightingUniforms,
273        max_bounces: u32,
274    ) -> Vector3D {
275        trace_bounces(ray, self, lights, max_bounces)
276    }
277
278    /// Finds the closest intersection between a ray and the scene
279    /// occluders.
280    ///
281    /// The winning occluder's [`Material`] is cloned exactly once, when the
282    /// returned [`Hit`] is constructed; losing candidates are never cloned.
283    /// Traversal itself resolves every candidate through the scene's
284    /// bounding volume hierarchy, so the cost scales with the parts of the
285    /// hierarchy the ray actually crosses rather than with the occluder
286    /// count.
287    ///
288    /// # Arguments
289    ///
290    /// - `&Ray` - The ray to test.
291    ///
292    /// # Returns
293    ///
294    /// - `Option<Hit>` - The closest hit, or `None` if the ray misses.
295    pub fn closest_hit(&self, ray: &Ray) -> Option<Hit> {
296        let occluders: &[Occluder] = self.get_occluders();
297        closest_hit_indexed(ray, self).map(
298            |(index, t, position, normal): (usize, f64, Vector3D, Vector3D)| Hit {
299                t,
300                position,
301                normal,
302                material: occluders[index].get_material().clone(),
303            },
304        )
305    }
306
307    /// Reports whether anything in the scene blocks the ray's parameter
308    /// range.
309    ///
310    /// This is the any-hit counterpart to [`RayTraceScene::closest_hit`],
311    /// built for shadow segments: it stops at the first blocker instead of
312    /// resolving the whole hierarchy, so a fully occluded light costs less
313    /// than a fully visible one. An empty scene reports `false`, and a ray
314    /// with an inverted or non-finite range reports `false` rather than
315    /// inventing a hit.
316    ///
317    /// # Arguments
318    ///
319    /// - `&Ray` - The ray to test.
320    ///
321    /// # Returns
322    ///
323    /// - `bool` - `true` when at least one occluder intersects the range.
324    pub fn occluded(&self, ray: &Ray) -> bool {
325        occluded_indexed(ray, self)
326    }
327}