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