euv_engine/raytracing/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 /// Intersects this ray with the triangle `v0`, `v1`, `v2` using the
62 /// Moller-Trumbore algorithm, keeping only hits inside this ray's
63 /// `t_min`..=`t_max` range.
64 ///
65 /// The test is two-sided: a hit from either face counts, and the
66 /// returned normal always faces against the ray direction.
67 ///
68 /// # Arguments
69 ///
70 /// - `Vector3D` - The first triangle vertex.
71 /// - `Vector3D` - The second triangle vertex.
72 /// - `Vector3D` - The third triangle vertex.
73 ///
74 /// # Returns
75 ///
76 /// - `Option<(f64, Vector3D)>` - The hit distance along the ray and the
77 /// unit normal oriented against the ray direction, or `None` on miss.
78 pub fn intersect_triangle(
79 &self,
80 v0: Vector3D,
81 v1: Vector3D,
82 v2: Vector3D,
83 ) -> Option<(f64, Vector3D)> {
84 let (t, normal): (f64, Vector3D) =
85 intersect_triangle(self.get_origin(), self.get_direction(), v0, v1, v2)?;
86 if t < self.get_t_min() || t > self.get_t_max() {
87 return None;
88 }
89 Some((t, normal))
90 }
91}
92
93/// Implements factory constructors for [`Occluder`].
94impl Occluder {
95 /// Creates a spherical occluder centered at `center` with `radius`.
96 ///
97 /// # Arguments
98 ///
99 /// - `Vector3D` - The sphere center.
100 /// - `f64` - The sphere radius.
101 /// - `Material` - The surface material.
102 ///
103 /// # Returns
104 ///
105 /// - `Occluder` - The new sphere occluder.
106 pub fn sphere(center: Vector3D, radius: f64, material: Material) -> Occluder {
107 Occluder {
108 kind: OccluderKind::Sphere,
109 center,
110 extent: Vector3D::new(radius, radius, radius),
111 vertices: [center; 3],
112 material,
113 }
114 }
115
116 /// Creates an axis-aligned bounding-box occluder from `min` to `max`.
117 ///
118 /// # Arguments
119 ///
120 /// - `Vector3D` - The AABB minimum corner.
121 /// - `Vector3D` - The AABB maximum corner.
122 /// - `Material` - The surface material.
123 ///
124 /// # Returns
125 ///
126 /// - `Occluder` - The new AABB occluder.
127 pub fn aabb(min: Vector3D, max: Vector3D, material: Material) -> Occluder {
128 Occluder {
129 kind: OccluderKind::Aabb,
130 center: min,
131 extent: max,
132 vertices: [min; 3],
133 material,
134 }
135 }
136
137 /// Creates a triangular occluder from its three vertices.
138 ///
139 /// The vertex order fixes the geometric winding; because the
140 /// intersection test is two-sided the winding does not affect which
141 /// faces produce hits, only the raw cross-product normal before it is
142 /// oriented against the incoming ray.
143 ///
144 /// # Arguments
145 ///
146 /// - `Vector3D` - The first triangle vertex.
147 /// - `Vector3D` - The second triangle vertex.
148 /// - `Vector3D` - The third triangle vertex.
149 /// - `Material` - The surface material.
150 ///
151 /// # Returns
152 ///
153 /// - `Occluder` - The new triangle occluder.
154 pub fn triangle(v0: Vector3D, v1: Vector3D, v2: Vector3D, material: Material) -> Occluder {
155 Occluder {
156 kind: OccluderKind::Triangle,
157 center: Vector3D::zero(),
158 extent: Vector3D::zero(),
159 vertices: [v0, v1, v2],
160 material,
161 }
162 }
163
164 /// Returns a list of `(center, radius)` sphere tuples approximating
165 /// this occluder, suitable for [`soft_shadow_factor`].
166 ///
167 /// For sphere occluders this returns `(center, radius)`. For AABB
168 /// occluders the bounding sphere is computed conservatively from the
169 /// AABB extents.
170 ///
171 /// # Returns
172 ///
173 /// - `Vec<(Vector3D, f64)>` - One bounding sphere per occluder.
174 pub fn occluder_points(&self) -> Vec<(Vector3D, f64)> {
175 collect_occluder_points(std::slice::from_ref(self))
176 }
177}
178
179/// Implements the constructor and zero-allocation tracing entry points for
180/// [`RayTraceScene`].
181impl RayTraceScene {
182 /// Creates a new scene taking ownership of `occluders` and precomputing
183 /// the `(center, radius)` shadow bounding spheres used by
184 /// [`soft_shadow_factor`].
185 ///
186 /// # Arguments
187 ///
188 /// - `Vec<Occluder>` - All occluding surfaces in the scene.
189 ///
190 /// # Returns
191 ///
192 /// - `RayTraceScene` - The new scene with precomputed shadow data.
193 pub fn new(occluders: Vec<Occluder>) -> RayTraceScene {
194 let shadow_points: Vec<(Vector3D, f64)> = collect_occluder_points(&occluders);
195 RayTraceScene {
196 occluders,
197 shadow_points,
198 }
199 }
200
201 /// Iteratively traces a ray through the scene and returns the final
202 /// shaded color, using the [`RAYTRACE_DEFAULT_MAX_BOUNCES`] constant as
203 /// the bounce limit.
204 ///
205 /// Performs no heap allocation per ray or per bounce: the shadow
206 /// bounding spheres precomputed at construction are reused, and no
207 /// [`Material`] is cloned. Use [`RayTraceScene::trace_with_bounces`] to
208 /// override the bounce limit.
209 ///
210 /// # Arguments
211 ///
212 /// - `Ray` - The ray to trace.
213 /// - `&LightingUniforms` - Lighting parameters used during shading.
214 ///
215 /// # Returns
216 ///
217 /// - `Vector3D` - The final traced color.
218 pub fn trace(&self, ray: Ray, lights: &LightingUniforms) -> Vector3D {
219 self.trace_with_bounces(ray, lights, RAYTRACE_DEFAULT_MAX_BOUNCES)
220 }
221
222 /// Iteratively traces a ray through the scene with an explicit bounce
223 /// limit and returns the final shaded color.
224 ///
225 /// On a miss the ambient color scaled by the accumulated specular
226 /// throughput is added. On a hit the surface material is evaluated with
227 /// [`LightingUniforms::shade`] and, when the hit material has a
228 /// non-zero specular component, the trace continues with a reflected
229 /// ray up to `max_bounces` times (incrementing the ray's `depth` field
230 /// per bounce).
231 ///
232 /// # Arguments
233 ///
234 /// - `Ray` - The ray to trace.
235 /// - `&LightingUniforms` - Lighting parameters used during shading.
236 /// - `u32` - The maximum number of bounces allowed for this ray.
237 ///
238 /// # Returns
239 ///
240 /// - `Vector3D` - The final traced color.
241 pub fn trace_with_bounces(
242 &self,
243 ray: Ray,
244 lights: &LightingUniforms,
245 max_bounces: u32,
246 ) -> Vector3D {
247 trace_bounces(
248 ray,
249 self.get_occluders(),
250 self.get_shadow_points(),
251 lights,
252 max_bounces,
253 )
254 }
255
256 /// Finds the closest intersection between a ray and the scene
257 /// occluders.
258 ///
259 /// The winning occluder's [`Material`] is cloned exactly once, when the
260 /// returned [`Hit`] is constructed; losing candidates are never cloned.
261 ///
262 /// # Arguments
263 ///
264 /// - `&Ray` - The ray to test.
265 ///
266 /// # Returns
267 ///
268 /// - `Option<Hit>` - The closest hit, or `None` if the ray misses.
269 pub fn closest_hit(&self, ray: &Ray) -> Option<Hit> {
270 let occluders: &[Occluder] = self.get_occluders();
271 closest_hit_indexed(ray, occluders).map(
272 |(index, t, position, normal): (usize, f64, Vector3D, Vector3D)| Hit {
273 t,
274 position,
275 normal,
276 material: occluders[index].get_material().clone(),
277 },
278 )
279 }
280}