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}