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}