euv_engine/raytracing/fn.rs
1use super::*;
2
3/// Returns the AABB extents `(min, max)` of an [`Occluder`].
4///
5/// For AABB occluders this is `(center, extent)`. For sphere occluders
6/// the bounding box is computed from the center and the `.x` component of
7/// `extent` (the sphere radius).
8///
9/// # Arguments
10///
11/// - `&Occluder` - The occluder to bound.
12///
13/// # Returns
14///
15/// - `(Vector3D, Vector3D)` - The `(min, max)` corners of the AABB.
16fn occluder_aabb_extents(occluder: &Occluder) -> (Vector3D, Vector3D) {
17 match occluder.get_kind() {
18 OccluderKind::Aabb => (occluder.get_center(), occluder.get_extent()),
19 OccluderKind::Sphere => {
20 let center: Vector3D = occluder.get_center();
21 let radius: f64 = occluder.get_extent().get_x();
22 let r: Vector3D = Vector3D::new(radius, radius, radius);
23 (center - r, center + r)
24 }
25 }
26}
27
28/// Flattens every occluder into `(center, radius)` sphere tuples used by
29/// [`soft_shadow_factor`].
30///
31/// For sphere occluders the tuple is `(center, radius)`. For AABB
32/// occluders a conservative bounding sphere is computed from the AABB.
33pub(crate) fn collect_occluder_points(occluders: &[Occluder]) -> Vec<(Vector3D, f64)> {
34 let mut out: Vec<(Vector3D, f64)> = Vec::new();
35 for occ in occluders.iter() {
36 let (mn, mx): (Vector3D, Vector3D) = occluder_aabb_extents(occ);
37 let cx: f64 = (mn.get_x() + mx.get_x()) * 0.5;
38 let cy: f64 = (mn.get_y() + mx.get_y()) * 0.5;
39 let cz: f64 = (mn.get_z() + mx.get_z()) * 0.5;
40 let ex: f64 = (mx.get_x() - mn.get_x()) * 0.5;
41 let ey: f64 = (mx.get_y() - mn.get_y()) * 0.5;
42 let ez: f64 = (mx.get_z() - mn.get_z()) * 0.5;
43 let r: f64 = (ex * ex + ey * ey + ez * ez).sqrt();
44 out.push((Vector3D::new(cx, cy, cz), r));
45 }
46 out
47}
48
49/// Finds the closest intersection between a ray and a list of occluders
50/// without touching any [`Material`].
51///
52/// Returns the winning occluder's index alongside the hit data so callers
53/// can borrow the material directly from the occluder list instead of
54/// cloning it per candidate. The tie-breaking rule matches the historical
55/// behavior: the first occluder achieving the minimum `t` wins.
56///
57/// # Arguments
58///
59/// - `&Ray` - The ray to test.
60/// - `&[Occluder]` - The occluders to test against.
61///
62/// # Returns
63///
64/// - `Option<(usize, f64, Vector3D, Vector3D)>` - The occluder index, the
65/// hit distance `t`, the hit position, and the surface normal, or `None`
66/// if the ray misses.
67pub(crate) fn closest_hit_indexed(
68 ray: &Ray,
69 occluders: &[Occluder],
70) -> Option<(usize, f64, Vector3D, Vector3D)> {
71 let origin: Vector3D = ray.get_origin();
72 let dir: Vector3D = ray.get_direction();
73 let t_min: f64 = ray.get_t_min();
74 let t_max: f64 = ray.get_t_max();
75 let mut best: Option<(usize, f64, Vector3D, Vector3D)> = None;
76 for (index, occ) in occluders.iter().enumerate() {
77 let candidate: Option<(f64, Vector3D)> = match occ.get_kind() {
78 OccluderKind::Sphere => {
79 let center: Vector3D = occ.get_center();
80 let radius: f64 = occ.get_extent().get_x();
81 match ray_sphere_intersect(origin, dir, center, radius) {
82 Some((t, n)) if t >= t_min && t <= t_max => Some((t, n)),
83 _ => None,
84 }
85 }
86 OccluderKind::Aabb => {
87 let aabb_min: Vector3D = occ.get_center();
88 let aabb_max: Vector3D = occ.get_extent();
89 match ray_aabb_intersect(origin, dir, aabb_min, aabb_max) {
90 Some((t_near, _t_far, n)) if t_near >= t_min && t_near <= t_max => {
91 Some((t_near, n))
92 }
93 _ => None,
94 }
95 }
96 };
97 if let Some((t, n)) = candidate {
98 let keep_previous: bool = matches!(&best, Some(previous) if previous.1 <= t);
99 if !keep_previous {
100 let hit_pos: Vector3D = origin + dir.scaled(t);
101 best = Some((index, t, hit_pos, n));
102 }
103 }
104 }
105 best
106}
107
108/// Builds a reflected ray bouncing off a surface point with the given
109/// normal.
110///
111/// # Arguments
112///
113/// - `&Ray` - The incoming ray.
114/// - `Vector3D` - The world-space hit position.
115/// - `Vector3D` - The outward unit normal at the hit point.
116///
117/// # Returns
118///
119/// - `Ray` - A new ray originating at the hit point with the reflected
120/// direction, `t_min` reset to `RAYTRACE_DEFAULT_T_MIN`, `t_max` set to
121/// `RAYTRACE_DEFAULT_T_MAX`, and `depth` incremented by one.
122fn bounce_ray(ray: &Ray, position: Vector3D, normal: Vector3D) -> Ray {
123 let dir: Vector3D = ray.get_direction();
124 let dot: f64 = dir.dot(normal);
125 let reflected_dir: Vector3D = dir - normal.scaled(2.0 * dot);
126 Ray {
127 origin: position,
128 direction: reflected_dir,
129 t_min: RAYTRACE_DEFAULT_T_MIN,
130 t_max: RAYTRACE_DEFAULT_T_MAX,
131 depth: ray.get_depth() + 1,
132 }
133}
134
135/// Iteratively traces a ray against `occluders` using precomputed shadow
136/// bounding spheres, performing no heap allocation per bounce.
137///
138/// Color contributions are accumulated with a specular throughput: each
139/// bounce multiplies the throughput by the hit material's specular
140/// intensity, and a miss adds the ambient color scaled by the current
141/// throughput. The bounce loop stops when the ray misses, when `depth`
142/// reaches `max_bounces`, or when the hit material's specular intensity is
143/// not greater than [`EPSILON`], matching the behavior of the historical
144/// recursive formulation.
145///
146/// # Arguments
147///
148/// - `Ray` - The ray to trace.
149/// - `&[Occluder]` - All occluding surfaces in the scene.
150/// - `&[(Vector3D, f64)]` - Precomputed `(center, radius)` shadow bounding
151/// spheres, one per occluder.
152/// - `&LightingUniforms` - Lighting parameters used during shading.
153/// - `u32` - The maximum number of bounces allowed for this ray.
154///
155/// # Returns
156///
157/// - `Vector3D` - The final traced color.
158pub(crate) fn trace_bounces(
159 ray: Ray,
160 occluders: &[Occluder],
161 shadow_points: &[(Vector3D, f64)],
162 lights: &LightingUniforms,
163 max_bounces: u32,
164) -> Vector3D {
165 let ambient: Vector3D = lights.get_ambient();
166 let mut color: Vector3D = Vector3D::zero();
167 let mut throughput: f64 = 1.0;
168 let mut current: Ray = ray;
169 loop {
170 let (index, _t, position, normal): (usize, f64, Vector3D, Vector3D) =
171 match closest_hit_indexed(¤t, occluders) {
172 None => {
173 color += ambient.scaled(throughput);
174 break;
175 }
176 Some(hit) => hit,
177 };
178 let material: &Material = occluders[index].get_material();
179 color += lights
180 .shade(position, normal, material, shadow_points)
181 .scaled(throughput);
182 let spec: f64 = material.get_specular();
183 if current.get_depth() >= max_bounces || spec <= EPSILON {
184 break;
185 }
186 throughput *= spec;
187 current = bounce_ray(¤t, position, normal);
188 }
189 color
190}