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