euv_engine/raytracing/fn.rs
1use super::*;
2
3/// Returns the component-wise minimum of two vectors.
4///
5/// # Arguments
6///
7/// - `Vector3D` - The first vector.
8/// - `Vector3D` - The second vector.
9///
10/// # Returns
11///
12/// - `Vector3D` - The component-wise minimum.
13fn vector3d_min(a: Vector3D, b: Vector3D) -> Vector3D {
14 Vector3D::new(
15 a.get_x().min(b.get_x()),
16 a.get_y().min(b.get_y()),
17 a.get_z().min(b.get_z()),
18 )
19}
20
21/// Returns the component-wise maximum of two vectors.
22///
23/// # Arguments
24///
25/// - `Vector3D` - The first vector.
26/// - `Vector3D` - The second vector.
27///
28/// # Returns
29///
30/// - `Vector3D` - The component-wise maximum.
31fn vector3d_max(a: Vector3D, b: Vector3D) -> Vector3D {
32 Vector3D::new(
33 a.get_x().max(b.get_x()),
34 a.get_y().max(b.get_y()),
35 a.get_z().max(b.get_z()),
36 )
37}
38
39/// Returns the inclusive parameter interval of a single AABB slab along one
40/// ray axis, or `None` when the ray runs parallel to the slab outside it.
41///
42/// A parallel axis is not rejected outright: the interval becomes
43/// `(-inf, +inf)` when the origin lies inside the slab and the caller keeps
44/// only the two axes that actually constrain the ray.
45///
46/// # Arguments
47///
48/// - `f64` - The ray origin component on this axis.
49/// - `f64` - The ray direction component on this axis.
50/// - `f64` - The slab lower bound.
51/// - `f64` - The slab upper bound.
52///
53/// # Returns
54///
55/// - `Option<(f64, f64)>` - The `(enter, exit)` parameters, or `None` when
56/// the ray is parallel to the slab and outside it.
57fn slab_interval(
58 origin_component: f64,
59 dir_component: f64,
60 lower: f64,
61 upper: f64,
62) -> Option<(f64, f64)> {
63 if dir_component.abs() < RAYTRACE_TRIANGLE_EPSILON {
64 if origin_component < lower || origin_component > upper {
65 return None;
66 }
67 return Some((f64::NEG_INFINITY, f64::INFINITY));
68 }
69 let inverse: f64 = 1.0 / dir_component;
70 let t0: f64 = (lower - origin_component) * inverse;
71 let t1: f64 = (upper - origin_component) * inverse;
72 Some((t0.min(t1), t0.max(t1)))
73}
74
75/// Reports whether the ray segment `t_min`..=`t_max` can touch the AABB
76/// spanning `lower` to `upper`.
77///
78/// This is a conservative broad-phase reject: it never discards an occluder
79/// whose exact test could report a hit inside the requested range, because
80/// the AABB fully contains the occluder and every exact hit lies inside the
81/// AABB.
82///
83/// # Arguments
84///
85/// - `Vector3D` - The ray origin.
86/// - `Vector3D` - The ray direction.
87/// - `Vector3D` - The AABB minimum corner.
88/// - `Vector3D` - The AABB maximum corner.
89/// - `f64` - The minimum ray parameter considered a valid hit.
90/// - `f64` - The maximum ray parameter considered a valid hit.
91///
92/// # Returns
93///
94/// - `bool` - `true` when the segment may touch the AABB.
95fn ray_segment_overlaps_aabb(
96 origin: Vector3D,
97 dir: Vector3D,
98 lower: Vector3D,
99 upper: Vector3D,
100 t_min: f64,
101 t_max: f64,
102) -> bool {
103 let x: Option<(f64, f64)> =
104 slab_interval(origin.get_x(), dir.get_x(), lower.get_x(), upper.get_x());
105 let y: Option<(f64, f64)> =
106 slab_interval(origin.get_y(), dir.get_y(), lower.get_y(), upper.get_y());
107 let z: Option<(f64, f64)> =
108 slab_interval(origin.get_z(), dir.get_z(), lower.get_z(), upper.get_z());
109 match (x, y, z) {
110 (Some((x0, x1)), Some((y0, y1)), Some((z0, z1))) => {
111 let t_enter: f64 = x0.max(y0).max(z0);
112 let t_exit: f64 = x1.min(y1).min(z1);
113 t_enter <= t_exit && t_exit >= t_min && t_enter <= t_max
114 }
115 _ => false,
116 }
117}
118
119/// Intersects a ray with the triangle `v0`, `v1`, `v2` using the
120/// Moller-Trumbore algorithm.
121///
122/// The test is two-sided: a hit on either face counts, and the returned
123/// normal is flipped when needed so that it always faces against the ray
124/// direction, making back-face hits usable for shading. A ray parallel to
125/// the triangle plane yields `None`. No `t_min`/`t_max` filtering is
126/// applied here; use [`Ray::intersect_triangle`] for the range-clamped
127/// variant.
128///
129/// # Arguments
130///
131/// - `Vector3D` - The ray origin.
132/// - `Vector3D` - The ray direction (expected to be unit length).
133/// - `Vector3D` - The first triangle vertex.
134/// - `Vector3D` - The second triangle vertex.
135/// - `Vector3D` - The third triangle vertex.
136///
137/// # Returns
138///
139/// - `Option<(f64, Vector3D)>` - The hit distance along the ray and the
140/// unit surface normal oriented against the ray direction, or `None` on
141/// miss.
142pub fn intersect_triangle(
143 origin: Vector3D,
144 dir: Vector3D,
145 v0: Vector3D,
146 v1: Vector3D,
147 v2: Vector3D,
148) -> Option<(f64, Vector3D)> {
149 let edge1: Vector3D = v1 - v0;
150 let edge2: Vector3D = v2 - v0;
151 let pvec: Vector3D = dir.cross(edge2);
152 let det: f64 = edge1.dot(pvec);
153 if det.abs() < RAYTRACE_TRIANGLE_EPSILON {
154 return None;
155 }
156 let inverse_det: f64 = 1.0 / det;
157 let tvec: Vector3D = origin - v0;
158 let u: f64 = tvec.dot(pvec) * inverse_det;
159 if u < 0.0 || u > 1.0 {
160 return None;
161 }
162 let qvec: Vector3D = tvec.cross(edge1);
163 let v: f64 = dir.dot(qvec) * inverse_det;
164 if v < 0.0 || u + v > 1.0 {
165 return None;
166 }
167 let t: f64 = edge2.dot(qvec) * inverse_det;
168 let geometric: Vector3D = edge1.cross(edge2).normalized();
169 let normal: Vector3D = if geometric.dot(dir) > 0.0 {
170 -geometric
171 } else {
172 geometric
173 };
174 Some((t, normal))
175}
176
177/// Returns the AABB extents `(min, max)` of an [`Occluder`].
178///
179/// For AABB occluders this is `(center, extent)`. For sphere occluders
180/// the bounding box is computed from the center and the `.x` component of
181/// `extent` (the sphere radius). For triangle occluders the bounding box
182/// is the component-wise span of the three vertices.
183///
184/// # Arguments
185///
186/// - `&Occluder` - The occluder to bound.
187///
188/// # Returns
189///
190/// - `(Vector3D, Vector3D)` - The `(min, max)` corners of the AABB.
191fn occluder_aabb_extents(occluder: &Occluder) -> (Vector3D, Vector3D) {
192 let (mn, mx): (Vector3D, Vector3D) = match occluder.get_kind() {
193 OccluderKind::Aabb => (occluder.get_center(), occluder.get_extent()),
194 OccluderKind::Sphere => {
195 let center: Vector3D = occluder.get_center();
196 let radius: f64 = occluder.get_extent().get_x();
197 let r: Vector3D = Vector3D::new(radius, radius, radius);
198 (center - r, center + r)
199 }
200 OccluderKind::Triangle => {
201 let [v0, v1, v2]: [Vector3D; 3] = occluder.get_vertices();
202 let lo: Vector3D = vector3d_min(v0, vector3d_min(v1, v2));
203 let hi: Vector3D = vector3d_max(v0, vector3d_max(v1, v2));
204 (lo, hi)
205 }
206 };
207 (vector3d_min(mn, mx), vector3d_max(mn, mx))
208}
209
210/// Flattens every occluder into `(center, radius)` sphere tuples used by
211/// [`soft_shadow_factor`].
212///
213/// For sphere occluders the tuple is `(center, radius)`. For AABB
214/// occluders a conservative bounding sphere is computed from the AABB.
215///
216/// # Arguments
217///
218/// - `&[Occluder]` - The occluders to flatten into bounding spheres.
219///
220/// # Returns
221///
222/// - `Vec<(Vector3D, f64)>` - The `(center, radius)` sphere of each occluder.
223pub(crate) fn collect_occluder_points(occluders: &[Occluder]) -> Vec<(Vector3D, f64)> {
224 let mut out: Vec<(Vector3D, f64)> = Vec::new();
225 for occ in occluders.iter() {
226 let (mn, mx): (Vector3D, Vector3D) = occluder_aabb_extents(occ);
227 let cx: f64 = (mn.get_x() + mx.get_x()) * 0.5;
228 let cy: f64 = (mn.get_y() + mx.get_y()) * 0.5;
229 let cz: f64 = (mn.get_z() + mx.get_z()) * 0.5;
230 let ex: f64 = (mx.get_x() - mn.get_x()) * 0.5;
231 let ey: f64 = (mx.get_y() - mn.get_y()) * 0.5;
232 let ez: f64 = (mx.get_z() - mn.get_z()) * 0.5;
233 let r: f64 = (ex * ex + ey * ey + ez * ez).sqrt();
234 out.push((Vector3D::new(cx, cy, cz), r));
235 }
236 out
237}
238
239/// Finds the closest intersection between a ray and a list of occluders
240/// without touching any [`Material`].
241///
242/// Returns the winning occluder's index alongside the hit data so callers
243/// can borrow the material directly from the occluder list instead of
244/// cloning it per candidate. The tie-breaking rule matches the historical
245/// behavior: the first occluder achieving the minimum `t` wins.
246///
247/// Every occluder is first run through a broad-phase AABB slab test
248/// against the ray's `t_min`..=`t_max` segment; occluders whose bounds the
249/// segment provably cannot touch are skipped without any exact math. The
250/// test is conservative, so it only removes candidates that could not have
251/// produced a hit, leaving the returned hit identical to a full scan.
252///
253/// # Arguments
254///
255/// - `&Ray` - The ray to test.
256/// - `&[Occluder]` - The occluders to test against.
257///
258/// # Returns
259///
260/// - `Option<(usize, f64, Vector3D, Vector3D)>` - The occluder index, the
261/// hit distance `t`, the hit position, and the surface normal, or `None`
262/// if the ray misses.
263pub(crate) fn closest_hit_indexed(
264 ray: &Ray,
265 occluders: &[Occluder],
266) -> Option<(usize, f64, Vector3D, Vector3D)> {
267 let origin: Vector3D = ray.get_origin();
268 let dir: Vector3D = ray.get_direction();
269 let t_min: f64 = ray.get_t_min();
270 let t_max: f64 = ray.get_t_max();
271 let mut best: Option<(usize, f64, Vector3D, Vector3D)> = None;
272 for (index, occ) in occluders.iter().enumerate() {
273 let (bound_min, bound_max): (Vector3D, Vector3D) = occluder_aabb_extents(occ);
274 if !ray_segment_overlaps_aabb(origin, dir, bound_min, bound_max, t_min, t_max) {
275 continue;
276 }
277 let candidate: Option<(f64, Vector3D)> = match occ.get_kind() {
278 OccluderKind::Sphere => {
279 let center: Vector3D = occ.get_center();
280 let radius: f64 = occ.get_extent().get_x();
281 match ray_sphere_intersect(origin, dir, center, radius) {
282 Some((t, n)) if t >= t_min && t <= t_max => Some((t, n)),
283 _ => None,
284 }
285 }
286 OccluderKind::Aabb => match ray_aabb_intersect(origin, dir, bound_min, bound_max) {
287 Some((t_near, _t_far, n)) if t_near >= t_min && t_near <= t_max => {
288 Some((t_near, n))
289 }
290 _ => None,
291 },
292 OccluderKind::Triangle => {
293 let [v0, v1, v2]: [Vector3D; 3] = occ.get_vertices();
294 ray.intersect_triangle(v0, v1, v2)
295 }
296 };
297 if let Some((t, n)) = candidate {
298 let keep_previous: bool = matches!(&best, Some(previous) if previous.1 <= t);
299 if !keep_previous {
300 let hit_pos: Vector3D = origin + dir.scaled(t);
301 best = Some((index, t, hit_pos, n));
302 }
303 }
304 }
305 best
306}
307
308/// Builds a reflected ray bouncing off a surface point with the given
309/// normal.
310///
311/// # Arguments
312///
313/// - `&Ray` - The incoming ray.
314/// - `Vector3D` - The world-space hit position.
315/// - `Vector3D` - The outward unit normal at the hit point.
316///
317/// # Returns
318///
319/// - `Ray` - A new ray originating at the hit point with the reflected
320/// direction, `t_min` reset to `RAYTRACE_DEFAULT_T_MIN`, `t_max` set to
321/// `RAYTRACE_DEFAULT_T_MAX`, and `depth` incremented by one.
322fn bounce_ray(ray: &Ray, position: Vector3D, normal: Vector3D) -> Ray {
323 let dir: Vector3D = ray.get_direction();
324 let dot: f64 = dir.dot(normal);
325 let reflected_dir: Vector3D = dir - normal.scaled(2.0 * dot);
326 Ray {
327 origin: position,
328 direction: reflected_dir,
329 t_min: RAYTRACE_DEFAULT_T_MIN,
330 t_max: RAYTRACE_DEFAULT_T_MAX,
331 depth: ray.get_depth() + 1,
332 }
333}
334
335/// Iteratively traces a ray against `occluders` using precomputed shadow
336/// bounding spheres, performing no heap allocation per bounce.
337///
338/// Color contributions are accumulated with a specular throughput: each
339/// bounce multiplies the throughput by the hit material's specular
340/// intensity, and a miss adds the ambient color scaled by the current
341/// throughput. The bounce loop stops when the ray misses, when `depth`
342/// reaches `max_bounces`, or when the hit material's specular intensity is
343/// not greater than [`EPSILON`], matching the behavior of the historical
344/// recursive formulation.
345///
346/// # Arguments
347///
348/// - `Ray` - The ray to trace.
349/// - `&[Occluder]` - All occluding surfaces in the scene.
350/// - `&[(Vector3D, f64)]` - Precomputed `(center, radius)` shadow bounding
351/// spheres, one per occluder.
352/// - `&LightingUniforms` - Lighting parameters used during shading.
353/// - `u32` - The maximum number of bounces allowed for this ray.
354///
355/// # Returns
356///
357/// - `Vector3D` - The final traced color.
358pub(crate) fn trace_bounces(
359 ray: Ray,
360 occluders: &[Occluder],
361 shadow_points: &[(Vector3D, f64)],
362 lights: &LightingUniforms,
363 max_bounces: u32,
364) -> Vector3D {
365 let ambient: Vector3D = lights.get_ambient();
366 let mut color: Vector3D = Vector3D::zero();
367 let mut throughput: f64 = 1.0;
368 let mut current: Ray = ray;
369 loop {
370 let (index, _t, position, normal): (usize, f64, Vector3D, Vector3D) =
371 match closest_hit_indexed(¤t, occluders) {
372 None => {
373 color += ambient.scaled(throughput);
374 break;
375 }
376 Some(hit) => hit,
377 };
378 let material: &Material = occluders[index].get_material();
379 color += lights
380 .shade(position, normal, material, shadow_points)
381 .scaled(throughput);
382 let spec: f64 = material.get_specular();
383 if current.get_depth() >= max_bounces || spec <= EPSILON {
384 break;
385 }
386 throughput *= spec;
387 current = bounce_ray(¤t, position, normal);
388 }
389 color
390}